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

Каждая вычислительная машина имеет свой собственный язык программирования – язык команд или машинный язык – и может исполнять программы, записанные только на этом языке. С помощью машинного языка, в принципе, можно описать любой алгоритм, но затраты на программирование будут чрезвычайно велики. Это обусловлено тем, что машинный язык позволяет описывать и обрабатывать лишь примитивные структуры данных – бит, байт, слово. Программирование в машинных кодах требует чрезмерной детализации программы и доступно лишь программистам, хорошо знающим устройство и функционирование ЭВМ. Преодолеть эту трудность и позволили языки высокого уровня (Фортран, ПЛ/1, Паскаль, Си, Ада и др.) с развитыми структурами данных и средствами их обработки, не зависящими от языка конкретной ЭВМ.

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

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

Интерпретатор – это программа, которая в качестве входа допускает программу на входном языке и по мере распознавания конструкций входного языка реализует их, выдавая на выходе результаты вычислений, предписанные исходной программой.

Транслятор – это программа, которая допускает на входе исходную программу и порождает на своем выходе программу, функционально-эквивалентную исходной, называемую объектной . Объектная программа записывается на объектном языке. В частном случае, объектным языком может служить машинный язык, и в этом случае, полученную на выходе транслятора программу можно сразу же выполнить на ЭВМ (проинтерпретировать). При этом ЭВМ является интерпретатором объектной программы в машинных кодах. В общем случае объектный язык не обязательно должен быть машинным или близким к нему (автокодом). В качестве объектного языка может служить некоторый промежуточный язык – язык, лежащий между входным и машинным языками.

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

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

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

Интерпретаторы используются также и при эмуляции программ – исполнении на технологической машине программ, составленных для другой (объектной) машины. Данный вариант, в частности, используется при отладке на универсальной ЭВМ программ, которые будут выполняться на специализированной ЭВМ.

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

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

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

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


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

Слова в языке объединяются в более сложные конструкции – предложения. В языках программирования простейшим предложением является оператор. Предложения строятся из слов и более простых предложений по правилам синтаксиса. Синтаксис языка представляет собой описание правильных предложений. Описание смысла предложений, т.е. значений слов и их внутренних связей, составляет семантику языка. В дополнение отметим, что конкретная программа несет в себе некоторое воздействие на транслятор – прагматизм . В совокупности синтаксис, семантика и прагматизм языка образуют семиотику языка.

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

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

В соответствии с этой моделью входная программа, прежде всего, подвергается лексической обработке. Цель лексического анализа – перевод исходной программы на внутренний язык компилятора, в котором ключевые слова, идентификаторы, метки и константы приведены к одному формату и заменены условными кодами: числовыми или символьными, которые называются дескрипторами. Каждый дескриптор состоит из двух частей: класса (типа) лексемы и указателя на адрес в памяти, где хранится информация о конкретной лексеме. Обычно эта информация организуется в виде таблиц. Одновременно с переводом исходной программы на внутренний язык на этапе лексического анализа проводится лексический контроль - выявление в программе недопустимых слов.

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

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

Рис. 1.1. Упрощенная функциональная модель транслятора

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

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

Таковы основные аспекты процесса трансляции с языков высокого уровня. Подробнее организация различных фаз трансляции и связанные с ними практические способы их математического описания рассматриваются ниже.

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

Основные понятия

Программа представляет собой лингвистическое представление вычислений: i → P → P(i). Интерпретатор представляет собой программу, на вход которой подается программа Р и некоторые входные данные x. Он выполняет P на х: I(P, x) = P(x). Тот факт, что существует единственный транслятор, способный выполнять все возможные программы (которые можно представить в формальной системе), является очень глубоким и значительным открытием Тьюринга.

Процессор является интерпретатором программ на машинном языке. Как правило, слишком дорого писать интерпретаторы для языков высокого уровня, поэтому их транслируют в форму, которую интерпретировать легче.

Некоторые виды трансляторов имеют очень странные имена:

  • Ассемблер транслирует программы на ассемблере в машинный язык.
  • Компилятор транслирует с языка высокого уровня на язык более низкого.

Транслятор - это программа, которая принимает в качестве входных данных программу на некотором языке S и выдает программу на языке T таким образом, что обе они имеют ту же семантику: P → X → Q. То есть, ∀x. P(х) = Q(х).

Если транслировать всю программу в нечто интерпретируемое, то это называется компиляцией перед исполнением, или АОТ-компиляцией. АОТ-компиляторы могут использоваться последовательно, последний из которых часто является ассемблером, например:

Исходный код → Компилятор (транслятор) → Ассемблерный код → Ассемблер (транслятор) → Машинный код → ЦПУ (интерпретатор).

Оперативная или динамическая компиляция происходит, если часть программы транслируется, когда исполняются другие ранее скомпилированные части. JIT-трансляторы запоминают то, что они уже выполнили, чтобы не повторять исходный код снова и снова. Они способны даже производить адаптивную компиляцию и перекомпиляцию, основанную на поведении среды выполнения программы.

Многие языки позволяют выполнять код во время трансляции и компилировать новый код во время выполнения программы.

Этапы трансляции

Трансляция состоит из этапов анализа и синтеза:

Исходный код → Анализатор → Концептуальное представление → Генератор (синтезатор) → Целевой код.

Это обусловлено такими причинами:

  • Любой другой способ не подходит. Пословный перевод просто не работает.
  • Хорошее инженерное решение: если нужно написать трансляторы для M исходных языков и N целевых, потребуется написать только M + N простых программ (полукомпиляторов), а не M × N комплексных (полных трансляторов).

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

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

Запись транслятора

Транслятор - это программа или техническое средство, в котором задействованы три языка: исходный, целевой и базисный. Их можно записать в Т-форме, расположив исходный слева, целевой справа и базисный ниже.

Существует три вида компиляторов:

  • Транслятор - это самокомпилятор, если у него исходный язык соответствует базисному.
  • Компилятор, у которого целевой язык равен базисному, называется саморезидентным.
  • Транслятор - это кросс-компилятор, если у него целевой и базисный языки различные.

Почему это важно?

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

  • форматировании текстов;
  • к базам данных;
  • расширенных компьютерных архитектурах;
  • обобщенных ;
  • графических интерфейсах;
  • языках сценариев;
  • контроллерах;
  • виртуальных машинах;
  • машинных переводах.

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

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

Всеобъемлющая технология

Технология компилятора охватывает множество различных областей информатики:

  • формальную теорию языка: грамматику, парсинг, вычислимость;
  • компьютерную архитектуру: наборы инструкций, RISC или CISC, конвейерную обработку, ядра, тактовые циклы и т.д.;
  • концепции языков программирования: например, управление последовательностью выполнения, условное выполнение, итерации, рекурсии, функциональное разложение, модульность, синхронизацию, метапрограммирование, область видимости, константы, подтипы, шаблоны, тип вывода, прототипы, аннотации, потоки, монады, почтовые ящики, продолжения, групповые символы, регулярные выражения, транзакционную память, наследование, полиморфизм, режимы параметров и т. д.;
  • абстрактные языки и виртуальные машины;
  • алгоритмы и регулярные выражения, алгоритмы парсинга, графические алгоритмы, обучение;
  • языки программирования: синтаксис, семантику (статическую и динамическую), поддержку парадигм (структурной, ООП, функциональной, логической, стековой, параллелизма, метапрограммирования);
  • создание ПО (компиляторы, как правило, крупные и сложные): локализацию, кэширование, компонентизацию, API-интерфейсы, повторное использование, синхронизацию.

Проектирование компилятора

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

  • Проблемы с исходным языком. Легко ли его скомпилировать? Есть ли препроцессор? Как обрабатываются типы? Имеются ли библиотеки?
  • Группировка проходов компилятора: одно- или многоходовая?
  • Степень желательной оптимизации. Быстрая и нечистая трансляция программы практически без оптимизации может быть нормальной. Чрезмерная оптимизация будет тормозить компилятор, но лучший код во время выполнения может того стоить.
  • Требуемая степень обнаружения ошибок. Может ли транслятор просто остановиться на первой ошибке? Когда он должен остановиться? Доверить ли компилятору исправление ошибок?
  • Наличие инструментов. Если исходный язык не является очень маленьким, сканер и генератор анализаторов являются обязательными. Также существуют генераторы генераторов кода, но они не так распространены.
  • Вид целевого кода для генерации. Следует выбирать из чистого, дополненного или виртуального машинного кода. Или просто написать входную часть, создающую популярные промежуточные представления, такие как LLVM, RTL или JVM. Или сделать трансляцию от исходного в исходный код на C или JavaScript.
  • Формат целевого кода. Можно выбрать переносимый образа памяти.
  • Перенацеливание. При множестве генераторов хорошо иметь общую входную часть. По этой же причине лучше иметь один генератор для многих входных частей.

Архитектура компилятора: компоненты

Это главные функциональные компоненты транслятора, генерирующего машинный код (если выходной программой является программа на С или виртуальная машина, то потребуется не так много этапов):

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

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

Лексический анализ (сканирование)

Сканер конвертирует поток знаков исходного кода в поток токенов, убирая пробелы, комментарии и расширяя макросы.

Сканеры часто встречаются с такими проблемами, как принимать или не принимать во внимание регистр, отступы, перевод строки и вложенные комментарии.

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

  • символы, отсутствующие в алфавите;
  • превышение числа знаков в слове или строке;
  • не закрытый знак или строковый литерал;
  • конец файла в комментарии.

Синтаксический анализ (парсинг)

Парсер преобразует последовательность токенов в абстрактное синтаксическое дерево. Каждый узел дерева сохраняется как объект с именованными полями, многие из которых сами являются узлами дерева. На этом этапе циклы отсутствуют. При создании парсера необходимо обратить внимание на уровень сложности грамматики (LL или LR) и выяснить, есть ли какие-либо правила снятия неоднозначности. Некоторые языки действительно требуют проведения семантического анализа.

Ошибки, встречающиеся на этом этапе, называются синтаксическими. Например:

  • k = 5 * (7 - y;
  • j = /5;
  • 56 = x * 4.

Семантический анализ

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

Очевидно, что набор правил допустимости у разных языков различный. Если компилируются Java-подобные языки, трансляторы могут найти:

  • множественные объявления переменной в пределах области ее действия;
  • ссылки на переменную до ее объявления;
  • ссылки на необъявленное имя;
  • нарушение правил доступности;
  • слишком большое или недостаточное количество аргументов при вызове метода;
  • несоответствие типов.

Генерация

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

Генерация кода производит реальный машинный код. В традиционных компиляторах для RISC-машин на первом этапе создается ассемблер с бесконечным числом виртуальных регистров. Для CISC-машин, вероятно, этого не произойдет.

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

хорошую работу на сайт">

Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.

Размещено на http://www.allbest.ru

Введение

1.1 Разбор сверху-вниз

1.2 Разбор снизу-вверх

1.2.1 LR(k) - грамматики

1.2.1.1 LR(0) - грамматики

1.2.2 LALR(1) - грамматики

2. Разработка транслятора

2.1 Анализ требований

2.2 Проектирование

2.2.1 Проектирование лексического анализатора

2.2.4 Программная реализация синтаксического анализатора

2.3 Кодирование

2.4 Тестирование

Заключение

Список использованных источников

Приложение А. Листинг программного текста транслятора

Приложение Б. Результаты тестирования

Приложение В. Схема программы транслятора

Введение

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

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

Языки программирования являются инструментами для решения задач в разных предметных областях, что определяет специфику их организации и различия по назначению. В качестве примера можно привести такие языки как Фортран, ориентированный на научные расчеты, 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). Знание семантики языка позволяет отделить ее от его синтаксиса и использовать для преобразования в другой язык (осуществить генерацию кода).

Целью данной курсовой работы является разработка учебного транслятора с заданного упрощенного текстового языка высокого уровня.

1. Методы грамматического разбора

Рассмотрим основные методы грамматического разбора.

1.1 Разбор сверху-вниз

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

1.1.1 LL(k) - языки и грамматики

Рассмотрим дерево вывода в процессе получения левого вывода цепочки. Промежуточная цепочка в процессе вывода состоит из цепочки из терминалов w, самого левого нетерминала A, недовыведенной части x:

-S--

/ \

/ -А-x-\

/ | \

-w---u----

Pисунок 1

Для продолжения разбора требуется заменить нетерминал A по одному из правил вида A:y. Если требуется, чтобы разбор был детерминированным (без возвратов), это правило требуется выбирать специальным способом. Говорят, что грамматика имеет свойство LL(k), если для выбора правила оказывается достаточно рассмотреть только wAx и первые k символов непросмотренной цепочки u. Первая буква L (Left, левый) относится к просмотру входной цепочки слева направо, вторая - к используемому левому выводу.

Определим два множества цепочек:

а) FIRST(x) - множество терминальных цепочек, выводимых из x, укороченных до k символов.

б) FOLLOW(A)- множество укороченных до k символов терминальных цепочек, которые могут следовать непосредственно за A в выводимых цепочках.

Грамматика имеет свойство LL(k), если из существования двух цепочек левых выводов:

S:: wAx: wzx:: wu

S:: wAx: wtx:: wv

из условия FIRST(u)=FIRST(v) следует z=t.

В случае k=1 для выбора правила для А, достаточно знать только нетерминал A и а - первый символ цепочки u:

- следует выбрать правило A:x, если а входит в FIRST(x),

- следует выбрать правило A:е, если а входит в FOLLOW(A).

LL(к)-свойство накладывает довольно сильные ограничения на грамматику. Например, LL(2) грамматика S: aS | a не обладает свойством LL(1), т.к. FIRST(aS)=FIRST(a)=a. В данном случае можно понизить величину k с помощью "факторизации" (вынесения множителя за скобку):

S: aA

A: S | e

Любая LL(k)-грамматика однозначна. Леворекурсивная грамматика не принадлежит классу LL(k) ни для какого k. Иногда удается преобразовать не LL(1)-грамматику в эквивалентную ей LL(1)-грамматику с помощью устранения левой рекурсии и факторизации. Однако проблема существования эквивалентной LL(k)-грамматики для произвольной не LL(k)-грамматики неразрешима.

1.1.2 Метод рекурсивного спуска

Метод рекурсивного спуска ориентирован на те случаи, когда компилятор программируется на одном из языков высокого уровня, когда допускается использование рекурсивных процедур.

Основная идея рекурсивного спуска состоит в том, что каждому нетерминалу грамматики соотвествует процедура, которая распознает любую цепочку, порождаемую этим нетерминалом. Эти процедуры вызывают друг друга, когда это требуется.

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

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

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

1.2 Разбор снизу-вверх

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

-S--

/ \

/-x- \

/ | \

--w--b--u-

Рисунок 2

Промежуточный вывод имеет вид xbu, где x - цепочка терминалов и нетерминалов, из которой выводится просмотренная часть терминальной цепочки w, bu - непросмотренная часть терминальной цепочки, b - очередной символ. Чтобы продолжить разбор, можно либо добавить символ b к просмотренной части цепочки (выполнить так называемый "сдвиг"), либо выделить в конце x такую цепочку z (x=yz), что к z можно применить одно из правил грамматики B:z и заменить x на цепочку yB (выполнить так называемую "свертку"):

-S-- -S--

/ \ / \

/-x-b- \ /yB- \

/ | \ / | \

--w--b--u- --w--b--u-

Рисунок 3 - После сдвига Рисунок 4 - После свертки

Если свертку применять только к последним символам x, то мы будем получать правые выводы цепочки. Такой разбор получил название LR, где символ L (Left,левый) относится к просмотру цепочки слева направо, а R (Right, правый) относится к получаемым выводам.

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

1.2.1 LR(k) - грамматики

Если в процессе LR-разбора принять детерминированное решение о сдвиге/свертке удается, рассматривая только цепочку x и первые k символов непросмотренной части входной цепочки u (эти k символов называют аванцепочкой), говорят, что грамматика обладает LR(k)-свойством.

-S--

/ \

/-x- \

--w----u--

Рисунок 5

Различие между LL(k)- и LR(k)-грамматиками в терминах дерева вывода:

-S-

/ | \

/ A \

/ / \ \

-w---v---u-

Рисунок 6

В случае LL(k)-грамматик однозначно определить правило, примененное к A, можно по w и первым k символам vu, а в случае LR(k)-грамматик - по w,v и первым k символам u. Это нестрогое рассуждение показывает, что LL(k)-языки < LR(k)-языки (при k > 0).

1.2.1.1 LR(0) - грамматики

Рассмотрим вначале наиболее простые грамматики этого класса - LR(0). При разборе строки LR(0)-языка можно вообще не использовать аванцепочку - выбор между сдвигом и сверткой делается на основании цепочки x. Так как в процессе разбора она изменяется только с правого конца, ее называют стеком. Будем считать, что в грамматике нет бесполезных символов и начальный символ не встречается в правых частях правил - тогда свертка к начальному символу сигнализирует об успешном завершении разбора. Попробуем описать множество цепочек из терминалов и нетерминалов, появляющихся в стеке в процессе всех LR-разборов (другими словами - всех правых выводов из грамматики).

Определим следующие множества:

L(A:v) - левый контекст правила A:v - множество состояний стека, непосредственно перед сверткой v в A в ходе всех успешных LR-разборов. Очевидно, каждая цепочка из L(A:v) кончается на v. Если у всех таких цепочек отрезать хвост v, то получится множество цепочек, встречающихся слева от A в процессе всех успешных правых выводов. Обозначим это множество L(A) - левый контекст нетерминала A.

Построим грамматику для множества L(A). Терминалами новой грамматики будут терминалы и нетерминалы исходной грамматики, нетерминалы новой грамматики обозначим ,... - их значениями будут левые контексты нетерминалов исходной грамматики. Если S - начальный символ исходной грамматики, то грамматика левых контекстов будет содержать правило : e - левый контекст S содержит пустую цепочку Для каждого правила исходной грамматики, например, A: B C d E

и добавим в новую грамматику правила:

: - L(B) включает L(A)

: B - L(C) включает L(A) B

: B C d - L(E) включает L(A) B C d

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

- регулярны. Из этого следует, что принадлежность цепочки левому контексту какого-либо нетерминала можно вычислять индуктивно с помощью конечного автомата, просматривая цепочку слева направо. Опишем этот процесс конструктивно.

Назовем LR(0)-ситуацией правило грамматики с одной отмеченной позицией между символами правой части правила. Например, для грамматики S:A; A:aAA; A:b существуют следующие LR(0)-ситуации: S:_A; S:A_; A:_aAA; A:a_AA; A:aA_A; A:aAA_; A:_b; A:b_. (позиция обозначена символом подчеркивания).

Будем говорить, что цепочка x согласована с ситуацией А:b_c, если x=ab и a принадлежит L(A). (Другими словами, LR-вывод может быть продолжен x_... = ab_...:: abc_...:: aA_...:: S_.) В этих терминах L(A:b) - множество цепочек, согласованных с ситуацией A:b_, L(A)

- цепочки, согласованные с ситуацией A:_b, для любого правила A:b.

Пусть V(u) - множество ситуаций, согласованных с u. Покажем, что функция V - индуктивна.

Если в множество V(u) входит ситуация A:b_cd, то ситуация A:bc_d принадлежит V(uc). (c - терминал или нетерминал; b, d - последовательности (может быть пустые) терминалов и нетерминалов). Других ситуаций вида A:b_d, с непустым b в V(uc) нет. Осталось добавить в V(uc) ситуации вида C:_..., для каждого нетерминала C, левый контекст которого содержит uc. Если ситуация A:..._C... (C-нетерминал) принадлежит множеству V(uc), то uc принадлежит L(C) и V(uc) включает в себя ситуации вида C:_... для всех C-правил грамматики.

V(e) содержит ситуации S:_... (S-начальный символ), а также ситуации A:_..., если нетерминал A встречается непосредственно после _ в ситуациях, уже включенных в V(e).

Наконец, мы готовы дать определение LR(0)-грамматики. Пусть u - содержимое стека в процессе LR-разбора, V(u)-множество LR(0) ситуаций, согласованных с u. Если V(u) содержит ситуацию вида А:x_ (x-последовательность терминалов и нетерминалов), то u принадлежит L(A:x) и допустима свертка x в A. Если V(u) содержит ситуацию A:..._a... (а-терминал), то допустим сдвиг. Говорят о конфликте сдвиг-свертка, если для одной цепочки u допустимы и сдвиг, и свертка. Говорят о конфликте свертка-свертка, если допустимы свертки по различным правилам.

Грамматика называется LR(0), если для всех состояний стека в процессе LR-вывода нет конфликтов сдвиг-свертка или свертка-свертка.

1.2.1.2 LR(k) - грамматики

Для выбора между сдвигом или сверткой в LR(0) разборе используется только состояние стека. В LR(k) разборе учитывается также k-первых символов непросмотренной части входной цепочки (так называемая аванцепочка). Для обоснования метода следует аккуратно повторить рассуждения предыдущего параграфа, внеся изменения в определения.

Будем включать в левый контекст правила также аванцепочку. Если в правом выводе применяется вывод wAu: wvu, то пара wv,FIRSTk(u) принадлежит Lk(A:v), а пара w,FIRSTk(u) - Lk(A). Множество левых контекстов, как и в случае LR(0), можно вычислять с помощью индукции по левой цепочке. Назовем LR(k)-ситуацией пару: правило грамматики с отмеченной позицией и аванцепочку длины не более k. Отделять правило от аванцепочки будем вертикальной чертой.

Будем говорить, что цепочка 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.

Если в Vк(w) входит ситуация A:b_cd|u, то ситуация A:bc_d|u будет принадлежать Vk(wc). Для каждой ситуации А:b_Cd|u из Vk(wc), каждого правила C:f и цепочки x, принадлежащей FIRSTk(du) надо добавить в Vk(wc) ситуацию C:_f|x.

Используем построенные множества LR(k)-состояний для разрешения вопроса сдвиг-свертка. Пусть u - содержимое стека, а x - аванцепочка. Очевидно, что свертка по правилу A:b может быть проведена, если Vk(u) содержит ситуацию A:b_|x. Решение вопроса о допустимости сдвига требует аккуратности, если в грамматике имеются e-правила. В ситуации A:b_c|t (c не пусто) сдвиг возможен, если c начинается с терминала и x принадлежит FIRSTk(ct). Неформально говоря, можно занести в стек самый левый символ правой части правила, подготавливая последующую свертку. Если c начинается с нетерминала (ситуация имеет вид A:b_Cd|t), то занести в стек символ, подготавливая свертку в C, можно только в случае, если C не порождает пустую цепочку. Например, в состоянии V(e)= S:_A|e; A:_AaAb|e,a, A:_|e,a нет допустимых сдвигов, т.к. при выводе из A терминальных цепочек на некотором шаге требуется применить правило A:e к нетерминалу A, находящемуся на левом конце цепочки.

Определим множество EFFk(x), состоящее из всех элементов множества FIRSTk(x), при выводе которых нетерминал на левом конце x (если он есть) не заменяется на пустую цепочку. В этих терминах сдвиг допустим, если в множестве Vk(u) есть ситуация А:b_c|t, c не пусто и x принадлежит EFFk(ct).

Грамматика называется LR(k)-грамматикой, если ни одно LR(k) состояние не содержит двух ситуаций A:b_|u и B:c_d|v, таких что u принадлежит EFFk(dv). Такая пара соответствует конфликту свертка-свертка, если d пусто, и конфликту сдвиг-свертка, если d не пусто.

На практике LR(k)-грамматики при k>1 не применяются. На это имеются две причины. Первая: очень большое число LR(k) состояний. Вторая: для любого языка, определяемого LR(k)-грамматикой, существует LR(1)-грамматика; более того, для любого детерминированного КС-языка существует LR(1)-грамматика.

Число LR(1)-состояний для практически интересных грамматик также весьма велико. LR(0) свойством такие грамматики обладают редко. На практике чаще используется промежуточный между LR(0) и LR(1) метод, известный под названиями и LALR(1).

1.2.2 LALR(1) - грамматики

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

LALR(1)-метод (Look Ahead - заглядывание вперед) заключается в следующем. Введем на множестве LR(1)-ситуаций отношение эквивалентности: будем считать две ситуации эквивалентными, если они различаются только аванцепочками. Например, ситуации A:Aa_Ab|e и A:Aa_Ab|a эквивалентны. Построим каноническое множество LR(1)-состояний и объединим состояния, состоящие из множества эквивалентных ситуаций.

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

2. Разработка транслятора

2.1 Анализ требований

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

Проектирование лексического анализатора;

Проектирование магазинного автомата;

Программная реализация синтаксического анализатора;

Разработка модуля интерпретации.

Разработка будет проведена с использованием операционной системы Windows XP на персональном компьютере IBM PC с процессором Intel Pentium IV.

Исходя из тенденций развития программного обеспечения для реализации учебного транслятора выбран язык программирования С# в среде Visual Studio 2010.

2.2 Проектирование

2.1.1 Проектирование лексического анализатора

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

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

Лексема может описываться двумя основными признаками. Одним из них является принадлежность лексемы определенному классу (переменные, константы, операции и т. д.) Второй признак определяет конкретный элемент данного класса.

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

Просмотр таблицы идентификаторов выполняет две основные функции:

а) запись нового имени в таблицу при обработке описания переменных;

б) поиск имени, ранее записанного в таблицу.

Это позволяет выявлять такие ошибочные ситуации, как множественное описание переменной и наличие неописанной переменной.

Разработка лексического анализатора заключается частично в моделировании различных автоматов для распознавания идентификаторов, констант, зарезервированных слов и т. д. Если лексемы разного типа начинаются с одного и того же сисмвола или одной и той же последовательности символов, может оказаться необходимым объединение их распознавания.

Запуская лексический анализатор, мы разбиваем нашу программу на лексемы, после чего каждая лексемы проходит проверку длины (лексема не может быть больше 11 символов). Пройдя успешно этот этап, мы проверяем правильность расположения лексем (ключевых слов var, begin, end, for, to, do, end_for). Затем анализируем лексемы переменные - они не должны содержать цифр в своем описании и повторяться. На последнем этапе проверяем правильность написания лексем (ключевые слова, неизвестные идентификаторы). Если хотя бы одна из проверок выдает ошибку, лексический анализатор выводит ошибку.

Схема программы работы лексического анализатора приведена в приложении B на рисунке В.1.

2.2.2 Проектирование магазинного автомата

Зададим следующую грамматику:

Г: {Vt, Va, I, R},

где Vt - это множесто терминальных символов, Va - множество нетерминальных символов, I - начальное состояние грамматики, R - множество правил грамматики.

Для данной граматики зададим множества терминальных и нетерминальных символов:

Составим правила для нашей грамматики Г и приведем их в таблице 1.

Таблица 1 - Правила грамматики

№ правила

Левая часть правила

Правая часть правила

f ID = EX t EX d LE n;

Продолжение таблицы 1.

№ правила

Левая часть правила

Правая часть правила

Обозначения лексем, перевод лексем в коды и список обозначений грамматики приведем в таблицах 2, 3, 4 соответственно.

Таблица 2 - Обозначения лексем

Обозначение лексемы

ключевое слово «begin» (начало описания действий)

ключевое слово «end» (окончание описания действий)

ключевое слово «var» (описание переменных)

ключевое слово «read» (оператор ввода данных)

ключевое слово «write» (оператор вывода данных)

ключевое слово «for» (оператор цикла)

ключевое слово «to»

ключевое слово «do»

ключевое слово «end_case» (окончание оператора цикла)

тип переменных «целый»

оперция сложение

операция вычитания

операция умножения

разделительный символ «:»

разделительный символ «;»

разделительный символ «(»

разделительный символ «)»

разделительный символ «,»

Обозначение лексемы

разделительный символ «=»

Таблица 3 - Перевод лексем в коды

<цифра>

<буква>

Таблица 4 - Список обозначений грамматики

Обозначение

Пояснение

Программа

Описание вычислений

Описание переменных

Список переменных

Оператор

Присваивание

Выражение

Подвыражение

Бинарные операции

Унарные операции

Список присваиваний

Идентификатор

Константа

Построим детерменированный восходящий распознаватель.

Рассмотрим следующие отношения, для того чтобы построить детерминированный восходящий распознаватель:

а)Если имеется символ группы В такой, что в некоторое правило грамматики входит цепочка АВ и существует символ хПЕРВ"(В), то будем считать, что между символами х и А определяются отношения х ПОСЛЕ А

б)Если в заданной грамматике имеетя правило В->бАб А,ВV a , б то между А и х определяется отношение А СВЕРТ х.

Вся наша грамматика остается прежней, то есть:

Г: {Vt, Va, I, R},

а правила грамматики Г приведены в таблице 5.

Таблица 5 - Правила грамматики

№ правила

Левая часть правила

Правая часть правила

f ID = EX t EX d LE n;?

Продолжение таблицы 5.

№ правила

Левая часть правила

Правая часть правила

Где? - маркер конца цепочки.

Определим некоторые случаи:

а)Идентификатор ID состоит из множества букв латинского алфавита, то есть будем считать, что u = { a, b, c, d, e, f,g, h, i,j,k, l,m, n, o, p,q,r,s, t, u, v, w, x, y, z}

б) Константа СО состоит из цифр, то есть будем считать, что k = {0,1,2,3,4,5,6,7,8,9}

Для того, чтобы наша грамматика является смешанной стратегией предшествования, необходимо, чтобы выполнялись следующие условия:

а) Отсутствие е - правил

б) Имеются правила при которых, х ПОСЛЕ А? А СВЕРТ х = ?

в) А -> бYг

и необходимо, чтобы В ПОСЛЕ х? В СВЕРТ х = ?

т.е в грамматике будут выполняться В ПОСЛЕ х либо А ПОСЛЕ х, где х - символ -предикат цепочки б.

а) ПЕРВ"(PG)={PG?}

ПЕРВ"(RG) = ПЕРВ(DE) = {RG, v,:, i,;}

ПЕРВ" (AL) = ПЕРВ (b LE e)= {AL, b, e}

ПЕРВ" (DE) = ПЕРВ (v LV: i;) = {DE, v,:, i,;}

ПЕРВ" (LV) = ПЕРВ (ID, LV) = { LV, ID }

ПЕРВ" (OP) ={OP, ID, CO}

ПЕРВ" (EQ) = ПЕРВ(ID = EX;) = {EQ, =,;}

ПЕРВ" (EX) = {EX, SB, -}

ПЕРВ" (BO) ={B0, +,*,-}

ПЕРВ" (SB) =ПЕРВ((EX)SB) ? ПЕРВ(OP) ? ПЕРВ (ВО)={SB, (,), OP, BO};

ПЕРВ" (LE) = ПЕРВ(EQ) = {LE, (,), =,;, f, t, d, n, w, r}

ПЕРВ" (UO) = {UO,-}

ПЕРВ" (ID)= ПЕРВ" (u) = {u}

ПЕРВ" (CO) = ПЕРВ" (k) = {k}ПЕРВ" (e) ={ e}

ПЕРВ" (b) ={ b}

ПЕРВ" (e) ={ e}

ПЕРВ" (v) ={ v}

ПЕРВ" (w) ={ w}

ПЕРВ" (r) ={ r}

ПЕРВ" (i) ={ i}

ПЕРВ" (f) ={ f}

ПЕРВ" (d) ={d}

ПЕРВ" (n) ={ n}

ПЕРВ" (c) ={ c}

ПЕРВ" (+) ={ +}

ПЕРВ" (*) ={ *}

ПЕРВ" (-) ={ -}

ПЕРВ" (,) ={,}

ПЕРВ" (;) ={;}

ПЕРВ" (:) ={:}

ПЕРВ" (=) = { = }

ПЕРВ" (() ={ (}

ПЕРВ" ()) ={) }

ПЕРВ" (u) ={u}

ПЕРВ" (k) ={k}

б) СЛЕД `(AL) = {?}?СЛЕД"(PG)={?,b,e}

СЛЕД ` (DE) = {?}?ПЕРВ"(AL)= {?, b, e }

СЛЕД ` (LV) = {?}?ПЕРВ"(:)= {?,:}

СЛЕД ` (OP) = {?}?ПЕРВ"(SB)= {?,;,), d, t, +, -, *}

СЛЕД ` (EQ) = {?}?ПЕРВ"(LE)={?, (,),;, f, =, t, d, n,w,r }

СЛЕД ` (EX) = {?}?ПЕРВ"(t)?ПЕРВ"(d)?ПЕРВ"(;)?ПЕРВ"())={?, t,d,;,)}

СЛЕД ` (BO) = {?}?ПЕРВ"(SB)= {?, (,), OP, BO}

СЛЕД ` (UO) = {?}?ПЕРВ"(SB)= {?, (,), OP, BO}

СЛЕД ` (SB) = {?}?СЛЕД"(EX)= {?, t,d,;,), +, *, -}

СЛЕД ` (LE) = {?} ?ПЕРВ"(e) ?ПЕРВ"(n) = {?, e, n}

СЛЕД `(ID)= {?}? СЛЕД" (OP) ? ПЕРВ" (=) ={?,;,), d, t, +, -, *, =}

СЛЕД `(CO) = {?}? СЛЕД" (OP)= {?,;,), d, t, +, -, *, =}

СЛЕД ` (b) ={?}?ПЕРВ"(LE)= {?, u, =,;}

СЛЕД ` (e) ={?}?СЛЕД"(AL)= {?, b}

СЛЕД ` (v) ={?}?ПЕРВ"(LV)= {?, u }

СЛЕД ` (w) ={?}?ПЕРВ"(()= {?, (}

СЛЕД ` (r) ={?}?ПЕРВ"(() = {?, (}

СЛЕД ` (i) ={?}?ПЕРВ"(;)= {?,; }

СЛЕД ` (f) ={?}?ПЕРВ"(ID) = {?, u}

СЛЕД ` (d) ={?}?ПЕРВ"(LE)= {?, u, =,;}

СЛЕД ` (n) ={?}?ПЕРВ"(i) = {?, i }

СЛЕД ` (+) ={?}?СЛЕД"(ВО) = {?, +,*,-}

СЛЕД ` (-) ={?}?СЛЕД"(ВО) = {?, +,*,-}

СЛЕД ` (*) ={?}?СЛЕД"(ВО) = {?, +,*,-}

СЛЕД ` (;) ={?}?СЛЕД" (DE)?СЛЕД `(LE1)?СЛЕД" (EQ) = {?, b, e, l, u }

СЛЕД ` (:) ={?}?ПЕРВ"(i)= {?, i }

СЛЕД ` (=) = {?}?ПЕРВ"(EX) = {? (,), u, k, +, -, *}

СЛЕД ` (() ={?}?ПЕРВ"(DE)= {?, v,:, i,;}

СЛЕД ` ()) ={?}? ПЕРВ"(;) = {?,; }

СЛЕД ` (,) ={?}? ПЕРВ"(LV) = {?, u }

СЛЕД `(u) ={?}? ПЕРВ" (ID)= { u, ?}

СЛЕД `(k) ={?}? ПЕРВ (CO)= {?, k}

в) PG ->DE AL

AL ПОСЛЕ DE = {b,e} ПОСЛЕ DE = {(b DE), (e DE) }

e ПОСЛЕ LE = {(e LE)}

LE ПОСЛЕ b = {(,), =,;, f, t, d, n, w, r} ПОСЛЕ b = {((b), ()b), (=b), (;b), (f b), (t b), (d b), (n b), (w b), (r b)}

;ПОСЛЕ i = {(; i)}

i ПОСЛЕ: = { (i:) }

: ПОСЛЕ LV = { (: LV) }

LV ПОСЛЕ v = { (ID, v) }

LV ПОСЛЕ, = {(ID,)}

ПОСЛЕ ID = {(,u)}

LE ПОСЛЕ EQ = {(,), =,;, f, t, d, n, w, r } ПОСЛЕ EQ = {((EQ), () EQ), (= EQ), (; EQ), (f EQ), (t EQ), (d EQ), (n EQ), (w EQ), (r EQ)}

LE -> r (DE);

; ПОСЛЕ) = {(;))}

) ПОСЛЕ DE = {((DE)}

DE ПОСЛЕ (= (= {(v)), (:)), (i)), (;)), (e))}

(ПОСЛЕ r = {((r)}

LE -> w (DE);

; ПОСЛЕ) = {(;))}

) ПОСЛ DE = {((DE)}

DE ПОСЛЕ (= {(v)), (:)), (i)), (;)), (e))}

(ПОСЛЕ w = {((w)}

LE -> f ID = EX t EX d LE n;

; ПОСЛЕ n = {(;n)}

n ПОСЛЕ LE = { (n, LE)}

LE ПОСЛЕ d = { ((,), =,;, f, t, d, n, w, r)} ? ПОСЛЕ d = {((d), ()d), (;d), (f d), (t d), (d d), (n d), (w d), (r d)}

d ПОСЛЕ EX = {(d, EX)}

EX ПОСЛЕ t = (BO, -) ? ПОСЛЕ t = {(BO t), (- t)}

t ПОСЛЕ EX = { t EX}

EX ПОСЛЕ = = {(BO, -) ? ПОСЛЕ = = {(BO =), (- =)}

ПОСЛЕ ID = {(= ID)}

ID ПОСЛЕ f = {(ID f)}

EQ -> ID = EX;

; ПОСЛЕ EX = {(; EX }

EX ПОСЛЕ = = (BO, -) ? ПОСЛЕ = = {(BO =), (- =)}

ПОСЛЕ u = { (=u)}

SB ПОСЛЕ UO = { (,), OP, BO } ПОСЛЕ UO = {((UO), (OP UO), (BO UO) }

) ПОСЛЕ EX = { ()EX) }

EX ПОСЛЕ (= (BO, -) ? ПОСЛЕ (= {(BO (), (- ()}

SB-> SB BO SB

SB ПОСЛЕ BO = ((,), OP, BO) ПОСЛЕ BO = {((BO), ()BO), (OP BO), (BO BO)}

BO ПОСЛЕ SB = {+,*,-} ПОСЛЕ SB = {(+SB), (*SB), (-SB)}

ID ПОСЛЕ u = {(u, u)}

г) PG ->DE AL

AL СВЕРТ PG = AL СВЕРТ СЛЕД" (PG) = {(AL ?)}

e СВЕРТ AL = e СВЕРТ СЛЕД"(AL)= {(eb), (e?)}

=; СВЕРТ СЛЕД"(DE) = {(;b), (;?)}

LV СВЕРТ LV = LV СВЕРТ СЛЕД" (LV) = {(LV:), (LV?)}

ID СВЕРТ LV = ID СВЕРТ СЛЕД" (LV) = {(ID:), (ID ?)}

; СВЕРТ LE =; СВЕРТ СЛЕД" (LE) = {(; e), (;?), (;n)}

LE -> f ID = EX t EX d LE n;

; СВЕРТ LE =; СВЕРТ СЛЕД" (LE) = {(; e), (;?), (;n)}

EQ СВЕРТ LE = EQ СВЕРТ СЛЕД" (LE) = {(EQ e), (EQ?), (EQ n)}

EQ -> ID = EX;

; СВЕРТ EQ =; СВЕРТ СЛЕД" (EQ) = {(; (), (;)), (;;), (;f), (;?), (;=), (;t), (;d), (;n), (;w), (;r)}

SB СВЕРТ EX = SB СВЕРТ СЛЕД" (EX) = {(SB t), (SB?), (SB d), (SB)), (SB;), (SB(), (SB=), (SBf), (SBn), (SBw), (SBr) }

) СВЕРТ SB = SB СВЕРТ СЛЕД" (SB) = {() t), ()?), () d), ())), ();)}

OP СВЕРТ SB = OP СВЕРТ СЛЕД" (SB) = {(OP t), (OP ?), (OP d), (OP)), (OP;)}

SB-> SB BO SB

SB СВЕРТ SB = SB СВЕРТ СЛЕД" (SB) = {(SB, t), (SBd), (SB;). (SB)), (SB+), (SB-), (SB*), (SB?) }

СВЕРТ UO = - СВЕРТ СЛЕД" (UO) = { (-?), (--)}

СВЕРТ BO = + СВЕРТ СЛЕД" (BO) = {(++), (+?), (+*), (+-)}

* СВЕРТ BO = * СВЕРТ СЛЕД" (BO) = {(*+), (*?), (**), (*-)}

СВЕРТ BO = - СВЕРТ СЛЕД" (BO) = {(-+), (-?), (-*), (--)}

ID СВЕРТ OP = ID СВЕРТ СЛЕД" (OP) = {(ID+), (ID?), (ID*), (ID-)}

CO СВЕРТ OP = CO СВЕРТ СЛЕД" (OP) = {(CO+), (CO?), (CO*), (CO-), (CO;), (COd), (COt), (CO))}

ID СВЕРТ ID = ID СВЕРТ СЛЕД" (ID) = {(ID)), (ID ?), (ID k), (ID+), (ID-), (ID*), (ID=), (IDt), (IDd))}

u СВЕРТ ID = l СВЕРТ СЛЕД" (ID) = {(u)), (u?), (uk), (u+), (u-), (u*), (u=), (ut), (ud))}

CO СВЕРТ CO = CO СВЕРТ СЛЕД" (CO) = (CO+), (CO?), (CO*), (CO-), (CO;), (COd), (COt), (CO))}

k СВЕРТ CO = k СВЕРТ СЛЕД" (CO) = (k+), (k?), (k*), (k-), (k;), (kd), (kt), (k))}

Обнаружена одна конфликтная ситуация при сворачивании правил

OP ->ID и ID -> u ID

Вводим ID1 -> ID, следовательно переписываем правило ID1 -> u ID

Следовательно, проведем операции свертка.

ID1 СВЕРТ ID = ID1 СВЕРТ СЛЕД" (ID) = {(ID1)), (ID1 ?), (ID1 k), (ID1+), (ID1-), (ID1*), (ID1=), (ID1t), (ID1d))}

Для каждой пары (х, А)? х ПОСЛЕ А строим функцию перехода, определяющее действие перенос??(S 0 , x, A) = (S 0 , A)

? (S0, b, DE) = (S0, DEb)

? (S0, e, DE) = (S0, DEe)

? (S0, e, LE) = (S0, LEe)

? (S0,), b) = (S0, b))

? (S0,;, b) = (S0, b;)

? (S0, (, b) = (S0, b()

? (S0, =, b) = (S0, b=)

? (S0, f, b) = (S0, bf)

? (S0, t, b) = (S0, bt)

? (S0, d, b) = (S0, bd)

? (S0, n, b) = (S0, bn)

? (S0, w, b) = (S0, bw)

? (S0, r, b) = (S0, br)

? (S0,;, i) = (S0, i;)

? (S0, i,:) = (S0, i:)

? (S0,: LV) = (S0, LV:)

? (S0, ID, v) = (S0, vID)

? (S0, ID,) = (S0,ID)

? (S0, u) = (S0, u,)

? (S0, (, EQ)= (S0, EQ()

? (S0,), EQ)= (S0, EQ))

? (S0, =, EQ)= (S0, EQ=)

? (S0,;, EQ)= (S0, EQ;)

? (S0, f, EQ)= (S0, EQf)

? (S0, t, EQ)= (S0, EQt)

? (S0, d, EQ)= (S0, EQd)

? (S0, n, EQ)= (S0, EQn)

? (S0, w, EQ)= (S0, EQw)

? (S0, r, EQ)= (S0, EQr)

? (S0,;,)) = (S0,);)

? (S0, (, DE) = (S0, DE()

? (S0, v,)) = (S0,)v)

? (S0,;,)) = (S0,);)

? (S0, i,)) = (S0,)i)

? (S0,:,)) = (S0,):)

? (S0, e,)) = (S0,)e)

? (S0, (, r) = (S0, r()

? (S0, (, w) = (S0, w()

? (S0,;, n) = (S0, n;)

? (S0, n, LE) = (S0, LEn)

? (S0, (, d) = (S0, d()

? (S0,), d) = (S0, d))

? (S0,;, d) = (S0, d;)

? (S0, f, d) = (S0, df)

? (S0, t, d) = (S0, dt)

? (S0, d, d) = (S0, dd)

? (S0, n, d) = (S0, dn)

? (S0, w, d) = (S0, dw)

? (S0, r, d) = (S0, dr)

? (S0, d, EX) = (S0, EXd)

? (S0, BO, t) = (S0, tBO)

? (S0, -, t) = (S0, t-)

? (S0, t, EX) = (S0, EXt)

? (S0, BO, =) = (S0, =BO)

? (S0, -, =) = (S0, =-)

? (S0, =, ID) = (S0, ID=)

? (S0, ID, f) = (S0, fID)

? (S0,;, EX) = (S0, EX;)

? (S0, =, u) = (S0, u=)

? (S0, (, UO) = (S0, UO()

? (S0, OP, UO) = (S0, UO OP)

? (S0, BO, UO) = (S0, UO BO)

? (S0,), EX) = (S0, EX))

? (S0, BO, () = (S0, (BO)

? (S0, BO, -) = (S0, -BO)

? (S0, (, BO) = (S0, BO()

? (S0,), BO) = (S0,)BO)

? (S0, OP, BO) = (S0, BOOP)

? (S0, +, SB) = (S0, SB+)

? (S0, *, SB) = (S0, SB*)

? (S0, -, SB) = (S0, SB-)

? (S0, u, u) = (S0, uu)

Для каждой пары (х,А)? А СВЕРТ х строим функцию перехода, определяющее действие свертка?? * (S 0 , x, бA) = (S 0 , В), где В->бA

? * (S 0 , AL, ?) = (S 0 , PG)

? * (S 0 , e, b) = (S 0 , AL)

? * (S 0 , n, ?) = (S 0 , AL)

? * (S 0 ,;, b) = (S 0 , DE)

? * (S 0 ,;, ?) = (S 0 , DE)

? * (S 0 ,;, e) = (S 0 , DE)

? * (S 0 , LV,:) = (S 0 , LV)

? * (S 0 , LV, ?) = (S 0 , LV)

? * (S 0 , ID, ?) = (S 0 , LV)

? * (S 0 , ID, e) = (S 0 , LV)

? * (S 0 ,;, e) = (S 0 , LE)

? * (S 0 ,;, ?) = (S 0 , LE)

? * (S 0 ,;, n) = (S 0 , LE)

? * (S 0 , EQ, n) = (S 0 , LE)

? * (S 0 , EQ, e) = (S 0 , LE)

? * (S 0 , EQ, ?) = (S 0 , LE)

? * (S 0 ,;, e) = (S 0 , LE)

? * (S 0 ,;, ?) = (S 0 , LE)

? * (S 0 ,;, () = (S 0 , EQ)

? * (S 0 ,;,)) = (S 0 , EQ)

? * (S 0 ,;, f) = (S 0 , EQ)

? * (S 0 ,;, =) = (S 0 , EQ)

? * (S 0 ,;, t) = (S 0 , EQ)

? * (S 0 ,;, d) = (S 0 , EQ)

? * (S 0 ,;, n) = (S 0 , EQ)

? * (S 0 ,;, w) = (S 0 , EQ)

? * (S 0 ,;, r) = (S 0 , EQ)

? * (S 0 , SB, ?) = (S 0 , EX)

? * (S 0 , SB, d) = (S 0 , EX)

? * (S 0 , SB,)) = (S 0 , EX)

? * (S 0 , SB,;) = (S 0 , EX)

? * (S 0 , SB, w) = (S 0 , EX)

? * (S 0 , SB, r) = (S 0 , EX)

? * (S 0 , SB, f) = (S 0 , EX)

? * (S 0 , SB, =) = (S 0 , EX)

? * (S 0 , SB, t) = (S 0 , EX)

? * (S 0 , SB, ?) = (S 0 , SB)

? * (S 0 , SB, () = (S 0 , SB)

? * (S 0 , SB,)) = (S 0 , SB)

? * (S 0 , SB, u) = (S 0 , SB)

? * (S 0 , SB, k) = (S 0 , SB)

? * (S 0 , SB, +) = (S 0 , SB)

? * (S 0 , SB, -) = (S 0 , SB)

? * (S 0 , SB, *) = (S 0 , SB)

? * (S 0 , SB, e) = (S 0 , SB)

? * (S 0 ,), t) = (S 0 , SB)

? * (S 0 ,), ?) = (S 0 , SB)

? * (S 0 ,), t) = (S 0 , SB)

(S 0 ,),)) = (S 0 , SB)

? * (S 0 ,),;) = (S 0 , SB)

? * (S 0 , -, ?) = (S 0 , UO)

? * (S 0 , -, -) = (S 0 , UO)

? * (S 0 , +, +) = (S 0 , BO)

? * (S 0 , +, ?) = (S 0 , BO)

? * (S 0 , +, *) = (S 0 , BO)

? * (S 0 , -, +) = (S 0 , BO)

? * (S 0 , -, ?) = (S 0 , BO)

? * (S 0 , -, *) = (S 0 , BO)

? * (S 0 , -, -)) = (S 0 , BO)

? * (S 0 , *, +) = (S 0 , BO)

? * (S 0 , *, ?) = (S 0 , BO)

? * (S 0 , *, *) = (S 0 , BO)

? * (S 0 , *, -)) = (S 0 , BO)

? * (S 0 , u, +) = (S 0 , BO)

? * (S 0 , u, ?)= (S 0 , BO)

? * (S 0 , u, *) = (S 0 , BO)

? * (S 0 , u, -)) = (S 0 , BO)

? * (S 0 , k, +) = (S 0 , BO)

? * (S 0 , k, ?) = (S 0 , BO)

? * (S 0 , k, *) = (S 0 , BO)

? * (S 0 , k, -)) = (S 0 , BO)

? * (S 0 , CO, ?) = (S 0 , OP)

? * (S 0 , CO, +) = (S 0 , OP)

? * (S 0 , CO, *) = (S 0 , OP)

? * (S 0 , CO, -) = (S 0 , OP)

? * (S 0 , CO,;) = (S 0 , OP)

? * (S 0 , CO, d) = (S 0 , OP)

? * (S 0 , CO, t) = (S 0 , OP)

? * (S 0 , ID, -) = (S 0 , OP)

? * (S 0 , ID, *) = (S 0 , OP)

? * (S 0 , ID, ?) = (S 0 , OP)

? * (S 0 , ID, () = (S 0 , OP)

? * (S 0 , ID,)) = (S 0 , OP)

? * (S 0 , ID, u) = (S 0 , OP)

? * (S 0 , ID, k) = (S 0 , OP)

? * (S 0 , ID, -) = (S 0 , OP)

? * (S 0 , ID, +) = (S 0 , OP)

? * (S 0 , u,)) = (S 0 , I OP)

? * (S 0 , ID1, *) = (S 0 , ID)

? * (S 0 , ID1, ?) = (S 0 , ID)

? * (S 0 , ID1, () = (S 0 , ID)

? * (S 0 , ID1,)) = (S 0 , ID)

? * (S 0 , ID1, u) = (S 0 , ID)

? * (S 0 , ID1, k) = (S 0 , ID)

? * (S 0 , ID1, -) = (S 0 , ID)

? * (S 0 , ID1, +) = (S 0 , ID)

? * (S 0 , u,)) = (S 0 , ID)

? * (S 0 , u, ?) = (S 0 , BO)

? * (S 0 , u, k) = (S 0 , ID)

? * (S 0 , u, *)) = (S 0 , ID)

? * (S 0 , u, -)) = (S 0 , ID)

? * (S 0 , u, +)) = (S 0 , ID)

? * (S 0 , u, d)) = (S 0 , ID)

? * (S 0 , u, t)) = (S 0 , ID)

? * (S 0 , u, =)) = (S 0 , ID)

? * (S 0 , CO, ?) = (S 0 , CO)

? * (S 0 , CO, +) = (S 0 , CO)

? * (S 0 , CO, -) = (S 0 , CO)

? * (S 0 , CO, *) = (S 0 , CO)

? * (S 0 , CO,;) = (S 0 , CO)

? * (S 0 , CO, d) = (S 0 , CO)

? * (S 0 , CO, t) = (S 0 , CO)

? * (S 0 , CO,)) = (S 0 , CO)

? * (S 0 , k, +) = (S 0 , CO)

? * (S 0 , k, -) = (S 0 , CO)

? * (S 0 , k, *) = (S 0 , CO)

? * (S 0 , k,;) = (S 0 , CO)

?? * (S 0 , k, d) = (S 0 , CO)

? * (S 0 , k, t) = (S 0 , CO)

? * (S 0 , k,)) = (S 0 , CO)

? * (S 0 , k, () = (S 0 , CO)

2.2.3 Программная реализация синтаксического анализатора

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

Для восходящего грамматического разбора для детерминированного восходящего распознавателя после приведения ее к нужному виду требуется с использованием функций ПОСЛЕ и СВЕРТ спроектировать магазинный автомат с подробным описанием всех переходов в рамках функции переходов.

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

Такт работы магазинного автомата без чтения входного символа (пустой такт);

Такт работы магазинного автомата с чтением входного символа.

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

Схема программы работы синтаксического анализатора приведена в приложении B на рисунке В.2.

2.2.4 Разработка модуля интерпретации

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

Рассмотрим основные принципы формирования и выполнения постфиксной формы записи выражений.

Основные правила преобразования инфиксной записи выражения в постфиксную заключаются в следующем.

Считанные операнды добавляются к постфиксной записи, операции записываются в стек.

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

Считанная открывающая скобка заносится в стек.

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

После считывания всего выражения, оставшиеся в стеке операции добавляются к постфиксной записи.

Постфиксная запись выражения позволяет производить его вычисление следующим образом.

Если лексема является операндом, то она записывается в стек. Если лексема является операцией, то указанная операция выполняется над последними элементами (последним элементом), записанными в стек, и эти элементы (элемент) заменяются в стеке результатом операции.

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

Схема работы интерпретатора приведена в приложении B на рисунке В.3.

2.3 Кодирование

Программа реализована на языке C# в среде программирования Visual Studio 2010. Текст программы представлен в приложении А.

В программе реализованно пять классов. С помощью класса MainForn реальзован пользовательский интерфейс. C помощью класса LexAnalysis реализован модуль лексического анализа, SynAnalysis - модуль синтаксического анализа, Intepreter - модуль интерпретации, ProgramisciJakPolska - вспомогательный класс перевода выражений в обратную польскую запись (постфиксную).

Назначение процедур и функций, реализованных в программе, описано в таблицах 6,7,8.

Таблица 6 - Назначение процедур и функций лексического анализа

Подобные документы

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

    курсовая работа , добавлен 06.08.2013

    Проектирование лексического и синтаксического анализаторов учебного языка. Правила преобразования логических выражений в ПОЛИЗ. Формирование триад, оптимизация их списка. Логическая структура программы. Тестирование модулей транслятора-интерпретатора.

    курсовая работа , добавлен 28.05.2013

    Общая характеристика и оценка возможностей языка программирования си-шарп, его сходные и отличительные черты от С++ и Java. Разработка с помощью данного языка программирования лексического и синтаксического анализатора. Составление таблиц разбора.

    курсовая работа , добавлен 11.06.2010

    Проектирование программы-анализатора, состоящей из двух частей: лексического анализатора, разбивающего исходный текст программы на лексемы и заполняющего таблицу имен; синтаксического анализатора, проверяющего соответствие текста заданной грамматике.

    курсовая работа , добавлен 14.06.2010

    Написание программы, которая выполняет лексический и синтаксический анализ входного языка программирования, порождает таблицу лексем с указанием их типов и значений, а также строит синтаксическое дерево; текст входного языка вводится с клавиатуры.

    курсовая работа , добавлен 23.02.2012

    Методика разработки и частичная реализация транслятора для языка "С" с использованием языка "С++", производящего разбиение на минимальные неделимые конструкции языка исходной цепочки символов основываясь на лексике языка. Анализ работы программы.

    курсовая работа , добавлен 19.03.2012

    Структура, классификация и требования к реализации компилятора. Проектирование и реализация анализирующей части компилятора языка С++. Способы реализации лексического анализа. Алгоритм работы синтаксического анализатора. Принципы программной реализации.

    курсовая работа , добавлен 26.01.2013

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

    курсовая работа , добавлен 02.07.2011

    Методы грамматического разбора. Разработка структуры учебного транслятора на базовом языке программирования Object Pascal в среде объектно-ориентированного визуального программирования Borland DELPHI 6.0 с использованием операционной системы Windows XP.

    курсовая работа , добавлен 12.05.2013

    Программная реализация настольного приложения с использованием языка программирования C#. Проектирование и структура пользовательского интерфейса, требования к нему и оценка функциональности. Разработка руководства пользователя и его использование.

РАЗДЕЛ 7. Трансляция, компиляция и интерпретация

Программа - это последовательность инструкций, предназначенных для выполнения компьютером. В настоящее время программы оформляются в виде текста, который записывается в файлы. Этот текст является результатом деятельности программиста и, несмотря на специфику формального языка, остаётся программой для программиста .

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

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

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

Транслятор обычно выполняет также диагностику ошибок, формирует словари идентификаторов, выдаёт для печати тексты программы и т.д.

Трансляция программы - преобразование программы, представленной на одном из языков программирования, в программу на другом языке и, в определённом смысле, равносильную первой.

Язык, на котором представлена входная программа, называется исходным языком , а сама программа - исходным кодом . Выходной язык называется целевым языком или объектным кодом .

Виды трансляторов

Трансляторы подразделяют:

· Адресный . Функциональное устройство, преобразующее виртуальный адрес (англ. Virtual address ) в реальный адрес (англ. Memory address ).

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

· Многопроходной . Формирует объектный модуль за несколько просмотров исходной программы.

· Обратный . То же, что детранслятор. См. также: декомпилятор, дизассемблер.

· Однопроходной . Формирует объектный модуль за один последовательный просмотр исходной программы.

· Оптимизирующий . Выполняет оптимизацию кода в создаваемом объектном модуле.

· Синтаксически-ориентированный (синтаксически-управляемый) . Получает на вход описание синтаксиса и семантики языка и текст на описанном языке, который и транслируется в соответствии с заданным описанием.

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



Трансляторы реализуются в виде компиляторов или интерпретаторов . С точки зрения выполнения работы компилятор и интерпретатор существенно различаются.

Компиля́тор (англ. compiler - составитель, собиратель) -транслятор, выполняющий преобразование программы, составленной на исходном языке, в объектный модуль. Программа, переводящая текст программы на языке высокого уровня, в эквивалентную программу на машинном языке.

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

Компиляция -трансляция программы, составленной на исходном языке, в объектный модуль. Осуществляется компилятором.

Компилировать - проводить трансляцию машинной программы с проблемно-ориентированного языка на машинно-ориентированный язык.

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

Интерпретатор (англ. interpreter - истолкователь, устный переводчик) переводит и выполняет программу строка за строкой . Интерпретатор берёт очередной оператор языка из текста программы, анализирует его структуру и затем сразу исполняет (обычно после анализа оператор транслируется в некоторое промежуточное представление или даже машинный код для более эффективного дальнейшего исполнения). Только после того как текущий оператор успешно выполнен, интерпретатор перейдёт к следующему. При этом если один и тот же оператор будет выполняться в программе многократно, интерпретатор будет выполнять его так как, как будто встретил впервые. Вследствие этого программы, в которых требуется осуществить большой объём вычислений, будут выполняться медленно. Кроме того, для выполнения программы на другом компьютере там тоже должен стоять интерпретатор – ведь без него текст является просто набором символов.



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

Различия между компиляцией и интерпретацией.

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

2. Откомпилированные программы работают быстрее, но интерпретируемые проще исправлять и изменять.

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

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

Практически все языки программирования низкого уровня и третьего поколения, вроде ассемблера, Си или Модулы-2, являются компилируемыми, а более высокоуровневые языки, вроде Python или SQL, - интерпретируемыми.

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

4. Трансляция и интерпретация - разные процессы: трансляция занимается переводом программ с одного языка на другой, а интерпретация отвечает за исполнение программ. Однако, поскольку целью трансляции как правило является подготовка программы к интерпретации, то эти процессы обычно рассматриваются вместе.

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

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


Процесс компиляции разделяется на несколько этапов:

1. Препроцессор. Исходная программа обрабатывается путём подстановки имеющихся макросов и заголовочных файлов.

2. Лексический и синтаксический анализ. Программа преобразовывается в цепочку лексем, а затем во внутреннее представление в виде дерева.

3. Глобальная оптимизация. Внутреннее представление программы неоднократно преобразовывается с целью сокращения размера и времени исполнения программы.

4. Генерация кода. Внутреннее представление преобразовывается в блоки команд процессора, которые преобразовываются в ассемблеровский текст или в объектный код.

5. Ассемблирование. Если генерируется ассемблерный текст, производится его ассемблирование с целью получения объектного кода.

6. Сборка. Сборщик соединяет несколько объектных файлов в исполняемый файл или библиотеку.

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

На этапе ЛА обнаруживаются некоторые (простейшие) ошибки (недопустимые символы, неправильная запись чисел, идентификаторов и др.).

Рассмотрим более подробно стадию лексического анализа.

Основная задача лексического анализа - разбить входной текст, состоящий из последовательности одиночных символов, на последовательность слов, или лексем, т.е. выделить эти слова из непрерывной последовательности символов. Все символы входной последовательности с этой точки зрения разделяются на символы, принадлежащие каким-либо лексемам, и символы, разделяющие лексемы (разделители). В некоторых случаях между лексемами может и не быть разделителей. С другой стороны, в некоторых языках лексемы могут содержать незначащие символы (например, символ пробела в Фортране). В Си разделительное значение символов-разделителей может блокироваться («\» в конце строки внутри «...»).

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

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

Таким образом, общая схема работы лексического анализатора такова. Сначала выделяется отдельная лексема (возможно, используя символы-разделители). Ключевые слова распознаются либо явным выделением непосредственно из текста, либо сначала выделяется идентификатор, а затем делается проверка на принадлежность его множеству ключевых слов.

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

Лексический анализатор может быть как самостоятельной фазой трансляции, так и подпрограммой, работающей по принципу «дай лексему». В первом случае (рис. 3.1, а) выходом анализатора является файл лексем, во втором (рис. 3.1, б) лексема выдается при каждом обращении к анализатору (при этом, как правило, признак класса лексемы возвращается как результат функции «лексический анализатор», а значение лексемы передается через глобальную переменную). С точки зрения обработки значений лексем, анализатор может либо просто выдавать значение каждой лексемы, и в этом случае построение таблиц объектов (идентификаторов, строк, чисел и т.д.) переносится на более поздние фазы, либо он может самостоятельно строить таблицы объектов. В этом случае в качестве значения лексемы выдается указатель на вход в соответствующую таблицу.

Рис. 3.1:

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

Основная задача синтаксического анализа - разбор структуры программы. Как правило, под структурой понимается дерево, соответствующее разбору в контекстно-свободной грамматике языка. В настоящее время чаще всего используется либо LL(1) - анализ (и его вариант - рекурсивный спуск), либо LR(1)-анализ и его варианты (LR(0), SLR(1), LALR(1) и другие). Рекурсивный спуск чаще используется при ручном программировании синтаксического анализатора, LR(1) - при использовании систем автоматизации построения синтаксических анализаторов.

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

На этапе контекстного анализа выявляются зависимости между частями программы, которые не могут быть описаны контекстно- свободным синтаксисом. Это в основном связи «описание- использование», в частности анализ типов объектов, анализ областей видимости, соответствие параметров, метки и другие. В процессе контекстного анализа строится таблица символов, которую можно рассматривать как таблицу имен, пополненную информацией об описаниях (свойствах) объектов.

Основным формализмом, использующимся при контекстном анализе, являются атрибутные грамматики. Результатом работы фазы контекстного анализа является атрибутированное дерево программы. Информация об объектах может быть как рассредоточена в самом дереве, так и сосредоточена в отдельных таблицах символов. В процессе контекстного анализа также могут быть обнаружены ошибки, связанные с неправильным использованием объектов.

Затем программа может быть переведена во внутреннее представление . Это делается для целей оптимизации и/или удобства генерации кода. Еще одной целью преобразования программы во внутреннее представление является желание иметь переносимый компилятор . Тогда только последняя фаза (генерация кода) является машинно-зависимой. В качестве внутреннего представления может использоваться префиксная или постфиксная запись, ориентированный граф, тройки, четверки и другие.

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

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

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

Транслятор (англ. translator - переводчик) - это программа-переводчик. Она преобразует программу, написанную на одном из языков высокого уровня, в программу, состоящую из машинных команд. Транслятор обычно выполняет также диагностику ошибок, формирует словари идентификаторов, выдаёт для печати тексты программы и т. д. Язык, на котором представлена входная программа, называется исходным языком, а сама программа - исходным кодом. Выходной язык называется целевым языком или объектным кодом.

В общем случае понятие трансляции относится не только к языкам программирования, но и к другим языкам - как формальным компьютерным (вроде языков разметки типа HTML), так и естественным (русскому, английскому и т. п.).

Виды трансляторов

    Диалоговый. Обеспечивает использование языка программирования в режиме разделения времени (англ.).

    Синтаксически-ориентированный (синтаксически-управляемый). Получает на вход описание синтаксиса и семантики языка и текст на описанном языке, который и транслируется в соответствии с заданным описанием.

    Однопроходной. Формирует объектный модуль за один последовательный просмотр исходной программы.

    Многопроходной. Формирует объектный модуль за несколько просмотров исходной программы.

    Оптимизирующий. Выполняет оптимизацию кода в создаваемом объектном модуле.

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

    Обратный. Для программы в машинном коде выдаёт эквивалентную программу на каком-либо языке программирования (см.: дизассемблер, декомпилятор).

Трансляторы реализуются в виде компиляторов или интерпретаторов. С точки зрения выполнения работы компилятор и интерпретатор существенно различаются.

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

Виды компиляторов

    Векторизующий. Транслирует исходный код в машинный код компьютеров, оснащённых векторным процессором.

    Гибкий. Сконструирован по модульному принципу, управляется таблицами и запрограммирован на языке высокого уровня или реализован с помощью компилятора компиляторов.

    Диалоговый. См.: диалоговый транслятор.

    Инкрементальный. Повторно транслирует фрагменты программы и дополнения к ней без перекомпиляции всей программы.

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

    Компилятор компиляторов. Транслятор, воспринимающий формальное описание языка программирования и генерирующий компилятор для этого языка.

    Отладочный. Устраняет отдельные виды синтаксических ошибок.

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

    Самокомпилируемый. Написан на том же языке, с которого осуществляется трансляция.

    Универсальный. Основан на формальном описании синтаксиса и семантики входного языка. Составными частями такого компилятора являются: ядро, синтаксический и семантический загрузчики.