Проект "Компилятор формул"
В данном параграфе изучаются простейшие вопросы теории компиляции, рассматриваются языки простейших арифметических формул и стекового калькулятора, а также задача автоматического перевода с первого на второй.
Первым вариантом компилятора — средства для автоматического перевода программ, является рекурсивный компилятор формул — достаточно простой, но обладающий рядом недостатков.
Другой, значительно более общий подход к решению этой задачи, осуществляется во втором программном проекте — стековом компиляторе формул. Этот проект, кроме реализации компилятора, предусматривает также и реализацию интерпретатора арифметических формул. Исходные тексты упомянутых программ в значительной мере оптимизированы, а для облегчения работы над проектами используется утилита make.
Дополнительная информация о различных подходах к реализации компиляторов может быть найдена в книге [9].
Стековый калькулятор
Стековый калькулятор, как это следует из его названия, представляет из себя некоторый объект, использующий хорошо известный нам контейнер — стек. С формальной точки зрения стековый калькулятор — это класс, реализующий следующий интерфейс.
Интерфейс стекового калькулятора
interface StackCalc { // Добавить число в стек. void push(int val); // Сложить. int add() throws Exception; // Вычесть. int sub() throws Exception; // Умножить. int mul() throws Exception; // Разделить. int div() throws Exception; // Показать вершину стека. int top() throws Exception; }
Методы push и top хорошо известны и комментариев не требуют, а семантика остальных четырех такова: из стека извлекаются (с помощью метода pop ) два верхних числа, над ними выполняется указанная в названии метода арифметическая операция, а результат кладется обратно в стек (с помощью метода push ). При этом в качестве первого аргумента арифметической операции берется тот из двух извлеченных элементов, который был положен в стек раньше другого. Если в момент вызова одного из этих четырех методов глубина стека меньше двух, возникает исключительная ситуация. При реализации данного интерфейса на базе ограниченного вектора выполнение метода push также может привести к исключительной ситуации, связанной с переполнением стека.
Стековый калькулятор можно использовать для вычисления значений различных
арифметических выражений типа . Нужно только написать
предварительно программу для вычисления значения.
С целью сокращения длины подобной программы будем записывать последовательность вызываемых методов в строку, разделяя их просто пробелом. Названия методов арифметических операций заменим на соответствующие им знаки действий ( +, -, * и / ), вместо вызова метода push с аргументом val будем записывать только его аргумент, а завершающий любую программу вызов метода top вообще включать в такую сокращенную запись программы не будем.
С использованием этих сокращений программа для вычисления выражения примет вид 5 7 8 + * 25 +. Рисунок 12.1
показывает последовательность состояний стека стекового калькулятора при
выполнении этой программы.
В рассмотренном примере программа для стекового калькулятора была написана
по исходной формуле нами. Основной задачей этого
параграфа
является реализация компилятора — программы, которая сможет
осуществлять преобразование формулы в программу для калькулятора автоматически.
С формальной точки зрения компилятор представляет собой программную
реализацию
некоторой функции , действующей из множества цепочек одного
языка
(в рассматриваемом случае это язык арифметических формул) в множество
цепочек другого
(язык программ стекового калькулятора)
таким образом, что
семантика цепочек
и
совпадает. Говоря другими словами,
компилятор
(часто называемый также транслятором ) реализует перевод с одного языка
на другой с сохранением смысла.
Напомним некоторые важнейшие определения, связанные с языками и грамматиками, которые были рассмотрены в "Высказывания и предикаты" .
Пусть — некоторый алфавит,
—
метаалфавит, т.е. какой-то
другой алфавит, не пересекающийся с
(
).
Элементы метаалфавита
называются метасимволами. Грамматикой
называется набор (
), где
— множество символов,
— множество метасимволов,
— множество правил
вывода вида:
, где
— какой-то
метасимвол,
— произвольная цепочка над
объединением двух
алфавитов, и для каждого
встречается хотя бы одно
правило с
в левой части (до стрелочки), а
—
так называемый стартовый метасимвол.
Содержательно каждое правило грамматики имеет смысл подстановки. Например,
строка означает возможность
замены
метасимвола
на цепочку
.
Начав со стартового
символа и пользуясь различными правилами грамматики, мы можем получать
различные цепочки из символов, которые называются выводимыми
цепочками.
Заметим, что если в цепочке встречается метасимвол, то ее можно
преобразовать
дальше, применив одно из правил грамматики с этим метасимволом в левой части.
Если же метасимволов в цепочке не осталось, то процесс ее преобразования
закончен и больше с цепочкой ничего сделать нельзя. По этой причине обычные
символы (из алфавита ) часто называют терминалами,
а метасимволы (из
) — нетерминалами.
Языком , порожденным грамматикой
,
называется множество всех
терминальных выводимых цепочек.
Для задания грамматики часто используют очень наглядную форму представления,
называемую нормальной формой Бэкуса-Наура (НФБН).
Набор правил задают при этом в виде совокупности правил со
стрелочками,
перечисляющими все возможные цепочки, на которые может быть заменен
каждый из метасимволов грамматики в процессе вывода, а стартовым метасимволом
считается тот, который присутствует в левой части самого первого правила.
Возьмем в качестве алфавита множество, состоящее
из четырех знаков
арифметических операций (
,
,
и
) и 26-и
идентификаторов от
до
, которыми будут
обозначаться произвольные
целые числа:
.
Тогда язык будет представлять из себя все возможные
программы
для стекового калькулятора, включая и неправильные.
Например, пустая цепочка
означает
вызов единственного метода pop, что приведет к возникновению
исключительной ситуации. Принадлежащая этому языку цепочка 2 + также
соответствует некорректной программе, ибо в момент вызова метода add
в стеке будет содержаться только один элемент.
Для описания языка правильных программ для стекового калькулятора можно
воспользоваться следующей грамматикой .
![\begin{tabular}{lrl}
\ \ \ \ \ & $e$ $\rightarrow$ & $e\ e\ -$ \\
& $\mid$& $e\ e\ +$ \\
& $\mid$& $e\ e\ *$ \\
& $\mid$& $e\ e\ /$ \\
& $\mid$& $a$ $\mid$ $b$ $\mid$ $\ldots$ $\mid$ $z$ \\
\end{tabular}](/sites/default/files/tex_cache/a6a066d0c3a6cb6e0d26e137268d8bc2.png)
Именно язык мы и будем рассматривать, как язык
правильных программ
для стекового калькулятора.