Методы грамматического разбора при разработке учебного транслятора. Проектирование лексического анализатора и магазинного автомата. Программная реализация синтаксического анализатора текстового языка высокого уровня. Разработка модуля интерпретации.
Аннотация к работе
Как известно, алгоритмические языки становятся доступными программисту лишь после создания трансляторов с этих языков. В качестве примера можно привести такие языки как Фортран, ориентированный на научные расчеты, C, предназначенный для системного программирования, Пролог, эффективно описывающий задачи логического вывода, Лисп, используемый для рекурсивной обработки списков. Поэтому можно отметить разнообразие форм представления операторов и выражений, различие в наборе базовых операций, снижение эффективности программирования при решении задач, не связанных с предметной областью. Для трансляторов с языка Фортран характерна агрессивная оптимизация результирующего машинного кода, которая становится возможной благодаря относительно простой семантике конструкций языка - в частности, благодаря отсутствию механизмов альтернативного именования переменных через указатели или ссылки. Большинство же языков работают в основном со скалярами, предоставляя для обработки массивов процедуры и функции, написанные программистами.Рассмотрим основные методы грамматического разбора. [7-11]При разборе сверху-вниз промежуточные выводы перемещаются по дереву по направлению от корня к листьям.Рассмотрим дерево вывода в процессе получения левого вывода цепочки. Промежуточная цепочка в процессе вывода состоит из цепочки из терминалов w, самого левого нетерминала Если требуется, чтобы разбор был детерминированным (без возвратов), это правило требуется выбирать специальным способом. Говорят, что грамматика имеет свойство LL(k), если для выбора правила оказывается достаточно рассмотреть только WAX и первые k символов непросмотренной цепочки u. Определим два множества цепочек: а) FIRST(x) - множество терминальных цепочек, выводимых из x, укороченных до k символов. б) FOLLOW(A)-множество укороченных до k символов терминальных цепочек, которые могут следовать непосредственно заМетод рекурсивного спуска ориентирован на те случаи, когда компилятор программируется на одном из языков высокого уровня, когда допускается использование рекурсивных процедур. Основная идея рекурсивного спуска состоит в том, что каждому нетерминалу грамматики соотвествует процедура, которая распознает любую цепочку, порождаемую этим нетерминалом. Рекурсивный спуск можно использовать для любой LL(1)-грамматики. каждому нетерминалу грамматики соотвествует процедура, которая начинается с перехода на вычисляемую метку и содержит код, соотвствующий каждому правилу для данного нетерминала.Рассмотрим разбор снизу-вверх, при котором промежуточные выводы перемещаются по дереву по направлению к корню. Промежуточный вывод имеет вид xbu, где x - цепочка терминалов и нетерминалов, из которой выводится просмотренная часть терминальной цепочки w, bu - непросмотренная часть терминальной цепочки, b - очередной символ. Чтобы продолжить разбор, можно либо добавить символ b к просмотренной части цепочки (выполнить так называемый "сдвиг"), либо выделить в конце x такую цепочку z (x=yz), что к z можно применить одно из правил грамматики B:z и заменить x на цепочку YB (выполнить так называемую "свертку"):-S---S-Если в процессе LR-разбора принять детерминированное решение о сдвиге/свертке удается, рассматривая только цепочку x и первые k символов непросмотренной части входной цепочки u (эти k символов называют аванцепочкой), говорят, что грамматика обладает LR(k)-свойством. В случае LL(k)-грамматик однозначно определить правило, примененное кБудем считать, что в грамматике нет бесполезных символов и начальный символ не встречается в правых частях правил - тогда свертка к начальному символу сигнализирует об успешном завершении разбора. Попробуем описать множество цепочек из терминалов и нетерминалов, появляющихся в стеке в процессе всех LR-разборов (другими словами - всех правых выводов из грамматики). Терминалами новой грамматики будут терминалы и нетерминалы исходной грамматики, нетерминалы новой грамматики обозначим ,... Если S - начальный символ исходной грамматики, то грамматика левых контекстов будет содержать правило : e - левый контекст S содержит пустую цепочку Для каждого правила исходной грамматики, например, A: B C d E и добавим в новую грамматику правила: : - L(B) включает L(A) Из этого следует, что принадлежность цепочки левому контексту какого-либо нетерминала можно вычислять индуктивно с помощью конечного автомата, просматривая цепочку слева направо.Если в правом выводе применяется вывод WAU: wvu, то пара wv,FIRSTK(u) принадлежит Lk(A:v), а пара w,FIRSTK(u) - Lk(A). Будем говорить, что цепочка x согласована с ситуацией А:b_c|t если существует LR-вывод: x_yz = ab_yz:: abc_z:: AA_z:: S_, и FIRSTK(z)=t. Правила индуктивного вычисления множества состояний Vk следующие: Vk(e) содержит ситуации S:_a|e для всех правил S:a, где S-начальный символ. Для каждой ситуации А:_Ba|u из Vk(e), каждого правила B:b и цепочки x, принадлежащей FIRSTK(au), надо добавить в Vk(e) ситуацию B:_b|x. Для каждой ситуации А:b_Cd|u из Vk(wc), каждого правила C:f и цепочки x, принадлежа
Приложение А. Листинг программного текста транслятора
Приложение Б. Результаты тестирования
Приложение В. Схема программы транслятора
Введение
Давно уже прошли те времена, когда прежде чем написать программу надо было понять и запомнить не один десяток машинных инструкций. Современный программист формулирует свои задачи на языках программирования высокого уровня и использует язык ассемблера лишь в исключительных случаях. Как известно, алгоритмические языки становятся доступными программисту лишь после создания трансляторов с этих языков. [2-6]
Языки программирования достаточно сильно отличаются друг от друга по назначению, структуре, семантической сложности, методам реализации. Это накладывает свои специфические особенности на разработку конкретных трансляторов.
Языки программирования являются инструментами для решения задач в разных предметных областях, что определяет специфику их организации и различия по назначению. В качестве примера можно привести такие языки как Фортран, ориентированный на научные расчеты, C, предназначенный для системного программирования, Пролог, эффективно описывающий задачи логического вывода, Лисп, используемый для рекурсивной обработки списков. Эти примеры можно продолжить. Каждая из предметных областей предъявляет свои требования к организации самого языка. Поэтому можно отметить разнообразие форм представления операторов и выражений, различие в наборе базовых операций, снижение эффективности программирования при решении задач, не связанных с предметной областью. Языковые различия отражаются и в структуре трансляторов. Лисп и Пролог чаще всего выполняются в режиме интерпретации изза того, что используют динамическое формирование типов данных в ходе вычислений. Для трансляторов с языка Фортран характерна агрессивная оптимизация результирующего машинного кода, которая становится возможной благодаря относительно простой семантике конструкций языка - в частности, благодаря отсутствию механизмов альтернативного именования переменных через указатели или ссылки. Наличие же указателей в языке C предъявляет специфические требования к динамическому распределению памяти.
Структура языка характеризует иерархические отношения между его понятиями, которые описываются синтаксическими правилами. Языки программирования могут сильно отличаться друг от друга по организации отдельных понятий и по отношениям между ними. Язык программирования PL/1 допускает произвольное вложение процедур и функций, тогда как в C все функции должны находиться на внешнем уровне вложенности. Язык C допускает описание переменных в любой точке программы перед первым ее использованием, а в Паскале переменные должны быть определены в специальной области описания. Еще дальше в этом вопросе идет PL/1, который допускает описание переменной после ее использования. Или описание можно вообще опустить и руководствоваться правилами, принятыми по умолчанию. В зависимости от принятого решения, транслятор может анализировать программу за один или несколько проходов, что влияет на скорость трансляции.
Семантика языков программирования изменяется в очень широких пределах. Они отличаются не только по особенностям реализации отдельных операций, но и по парадигмам программирования, определяющим принципиальные различия в методах разработки программ. Специфика реализации операций может касаться как структуры обрабатываемых данных, так и правил обработки одних и тех же типов данных. Такие языки, как PL/1 и APL поддерживают выполнение матричных и векторных операций. Большинство же языков работают в основном со скалярами, предоставляя для обработки массивов процедуры и функции, написанные программистами. Но даже при выполнении операции сложения двух целых чисел такие языки, как C и Паскаль могут вести себя по-разному.
Наряду с традиционным процедурным программированием, называемым также императивным, существуют такие парадигмы как функциональное программирование, логическое программирование и объектно-ориентированное программирование. Структура понятий и объектов языков сильно зависит от избранной парадигмы, что также влияет на реализацию транслятора.
Даже один и тот же язык может быть реализован нескольким способами. Это связано с тем, что теория формальных грамматик допускает различные методы разбора одних и тех же предложений. В соответствии с этим трансляторы разными способами могут получать один и тот же результат (объектную программу) по первоначальному исходному тексту.
Вместе с тем, все языки программирования обладают рядом общих характеристик и параметров. Эта общность определяет и схожие для всех языков принципы организации трансляторов.
Языки программирования предназначены для облегчения программирования. Поэтому их операторы и структуры данных более мощные, чем в машинных языках.
Для повышения наглядности программ вместо числовых кодов используются символические или графические представления конструкций языка, более удобные для их восприятия человеком.
Для любого языка определяется: - множество символов, которые можно использовать для записи правильных программ (алфавит), основные элементы, - множество правильных программ (синтаксис), - "смысл" каждой правильной программы (семантика).
Независимо от специфики языка любой транслятор можно считать функциональным преобразователем F, обеспечивающим однозначное отображение X в Y, где X - программа на исходном языке, Y - программа на выходном языке. Поэтому сам процесс трансляции формально можно представить достаточно просто и понятно: Y = F(X).
Формально каждая правильная программа X - это цепочка символов из некоторого алфавита A, преобразуемая в соответствующую ей цепочку Y, составленную из символов алфавита B.
Язык программирования, как и любая сложная система, определяется через иерархию понятий, задающую взаимосвязи между его элементами. Эти понятия связаны между собой в соответствии с синтаксическими правилами. Каждая из программ, построенная по этим правилам, имеет соответствующую иерархическую структуру.
В связи с этим для всех языков и их программ можно дополнительно выделить следующие общие черты: каждый язык должен содержать правила, позволяющие порождать программы, соответствующие этому языку или распознавать соответствие между написанными программами и заданным языком.
Другой характерной особенностью всех языков является их семантика. Она определяет смысл операций языка, корректность операндов. Цепочки, имеющие одинаковую синтаксическую структуру в различных языках программирования, могут различаться по семантике (что, например, наблюдается в C , Pascal, Basic). Знание семантики языка позволяет отделить ее от его синтаксиса и использовать для преобразования в другой язык (осуществить генерацию кода).
Целью данной курсовой работы является разработка учебного транслятора с заданного упрощенного текстового языка высокого уровня.