Простейший учебный компилятор,
разработанный с помощью утилит Lex и Yacc

Содержание

Для учебных целей разработан простой компилятор simpcomp (от англ. Simple Compiler), реализованный с помощью утилит Lex и Yacc (вернее, их свободных версий flex и bison). В исходном языке специально опущены некоторые существенные возможности (логические переменные, инициализация переменных при описании, стандартные функции, разбиение программы на функции и их вызов и т.п.) — они оставлены в качестве задач для самостоятельного решения.

Описание входного языка

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

    while (условие) {
        тело цикла
    }
в учебном языке записывается как
    while (условие)
        тело цикла
    endwhile
(в роли закрывающей фигурной скобки используется оператор endwhile, а открывающая фигурная скобка вообще не нужна).

Кроме того, в число управляющих конструкций учебного языка добавлен оператор выбора, отсутствующий в C:
     if (условие1) ... elseif (условие2) ... elseif (условие3) ... else ... endif
Например, следующий фрагмент вычисляет функцию signum — знак числа x:

    double x;
    int sgn;
    . . .
    if (x > 0.)
        sgn = 1;
    elseif (x < 0.)
        sgn = -1;
    else
        sgn = 0;
    endif

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

Типы данных и переменные

Типы данных ограничиваются базовыми типами и массивами. Базовых типов всего 3: целое число int, вещественное число double и текстовая строка string. При описании массива размер его может быть любым выражением (а не только константой, как в C/C++). Примеры описаний переменных:

    int n; double x, y;
    double a[10];
    n = 1000;
    string s[n];
    double b[2*n + 1];

Выражения

В выражениях можно использовать 4 обычные арифметические операции и операцию взятия остатка от деления % (как и в C). Присваивание, в отличие от C, не является операцией. Стандартные функции, такие как sin, cos, exp в языке не реализованы (реализация оставлена в качестве задачи). Числовые константы записываются так же, как и в языке C (правда, целочисленные восьмеричные и шестнадцатеричные константы не реализованы). Строковые константы записываются в двойных апострофах: "abc", "x=" и т.п.

Оператор присваивания

Оператор присваивания имеет вид

    переменная = выражение;
где переменная в левой части может быть простой переменной или элементом массива. Оператор заканчивается символом ";" (точка с запятой). Примеры:
    int x, y, a[100];
    x = 5;
    y = 2*x + 1;
    a[x] = y;

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

    string s; double x;
    x = 1.5;
    s = x/2.;
После выполнения этого фрагмента строковая переменная s приобретет значение "0.75".

Численным переменным также можно присваивать строковое значение, в этом случае строка интерпретируется как текстовая запись числа, его значение присваивается переменной. Например, при выполнении фрагмента

    string s; int n; double x;
    s = "2.25";
    n = s; x = s;
переменной n присваивается значение 2, переменной x — значение 2.25.

Структурные операторы

Структурных операторов всего 2 типа: оператор цикла while и условный оператор if. Условный оператор может иметь 3 формы:
— оператор "если" if... endif
— оператор "если-иначе" if... else... endif
— оператор выбора "если - иначе если - иначе если -... иначе":
     if... elseif... elseif... else... endif

Структурные операторы имеют строку-заголовок и строку-окончание. В окончании к ключевому слову, с которого начинается строка-заголовок, добавляется префикс "end": if-endif, while-endwhile. Фигурные скобки или ключевые слова begin-end для указания границ структурных операторов становятся ненужными, а программа более ясной; кроме того, это позволяет избежать неоднозначностей в грамматике языка и конфликтов при разборе, которые присутствуют, например, в языке C при разборе конструкции

    if (условие1) if (условие2) оператор1; else оператор2;
(непонятно, к какому if относится else — в C считается, что к внутреннему; при LR-разборе конфликт сдвиг-свертка в данном случае разрешается в пользу сдвига).

Примеры использования структурных операторов.
1. Суммирование массива:

    int i; double x; double a[100];
    . . .
    s = 0.; i = 0;
    while (i < 100)
        s = s + a[i];
        i = i + 1;
    endwhile
2. Определение окончания множественного числа в русском языке (0 мячей, 1 мяч, 2 мяча, 3 мяча, 4 мяча, 5 мячей, 6 мячей...):
    int n;
    string numBalls;
    numBalls = "";
    . . .
    numBalls = numBalls+n; // число n преобразуется
             // к текстовому виду и добавляется в конец строки
    numBalls = numBalls + " мяч";
    if (n == 0 || n >= 5)
        numBalls = numBalls + "ей";
    elseif (n >= 2)
        numBalls = numBalls + "а";
    endif

Для выхода из цикла while можно, как и в C, использовать оператор break;
для перехода к следующей итерации цикла — оператор continue;

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

    double x, y;
    . . .
    if (y > 0. && x / y > 10.)
        x = 10. * y;
    endif
В отличие от C, целочисленные выражения нельзя использовать в качестве логических. Например, фрагмент программы
    int x, y;
    . . .
    if (x)
        y = 1;
    endif
является ошибочным (впрочем, и в C это дурной стиль!). Вместо этого надо писать
    int x, y;
    . . .
    if (x != 0)
        y = 1;
    endif
(такому стилю хорошо бы следовать и при программировании на C).

Реализация логического типа bool и логических переменных оставлена в качестве задачи для самостоятельного решения.

Операторы ввода / вывода

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

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

    int n;
    input n;
    double a[n];
    int i;
    i = 0;
    while (i < n)
        input a[i];
    endwhile
Здесь мы вводим с клавиатуры сначала длину n массива, затем создаем массив a длины n и в цикле вводим все его элементы.

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

    int n, i;
    input n;
    double a[n]
    ...
    outputln "n=", n;
    i = 0;
    while (i < n)
        output a[i], " ";
        i = i + 1;
        if (i % 5 == 0)
            outputln;
        endif
    endwhile
    outputln;
Здесь сначала печатается значение целочисленной переменной n, предваряемое текстом "n=", а затем после перевода строки — n элементов вещественного массива a по 5 элементов в каждой строке. После окончания печати массива выводится еще один символ перевода строки.

Примеры программ

1. Печать квадратов первых 20 натуральных чисел — файл "square20.s":

    int n;
    n = 1;
    while (n <= 20)
        outputln n, " ", n*n;
        n = n+1;
    endwhile
    end
Вывод программы:
    1 1
    2 4
    3 9
    4 16
    5 25
    6 36
    7 49
    8 64
    9 81
    10 100
    11 121
    12 144
    13 169
    14 196
    15 225
    16 256
    17 289
    18 324
    19 361
    20 400

2. Печать n первых простых чисел — файл "nprimes.s":

    int n, k, p;
    n = 50;
    outputln "Number of primes:";
    input n;
    k = 0;
    p = 2;
    while (k < n)
        if (p == 2)
            output " ", p;
            k = k+1;
            p = p+1;
        else
            int d, prime;
            prime = 1;
            d = 3;
            while (prime == 1 && d*d <= p)
                if (p % d == 0)
                    prime = 0;
                    break;
                endif
                d = d+2;
            endwhile
            if (prime == 1)
                output " ", p;
                k = k+1;
                if (k % 10 == 0)
                    outputln;
                endif
            endif
            p = p+2;
        endif
    endwhile
    outputln;

    end
Вывод программы (для n=50):
    Number of primes:
    50
     2 3 5 7 11 13 17 19 23 29
     31 37 41 43 47 53 59 61 67 71
     73 79 83 89 97 101 103 107 109 113
     127 131 137 139 149 151 157 163 167 173
     179 181 191 193 197 199 211 223 227 229

3. Приближенное вычисление квадратного корня вещественного числа — файл "sqrt.s":

    double x0, x, a;
    double EPS;
    EPS = 0.0000001;
    while (0 == 0)
        outputln "Input a number:";
        input a;
        if (a <= 0.)
            break;
        endif
        x = a;
        if (x < 1.)
            x = 1.;
        endif
        double xPrev;
        xPrev = x + 1.;
        while (xPrev - x > EPS)
            xPrev = x;
            x = (x + a/x) / 2.;
            outputln x;
        endwhile
        outputln "sqrt(", a, ") = ", x;
    endwhile
    end
Вывод программы для чисел 2 и 10 (для заданного числа печатаются последовательность приближений и окончательный результат):
    Input a number:
    2
    1.5
    1.41667
    1.41422
    1.41421
    1.41421
    sqrt(2) = 1.41421
    Input a number:
    10
    5.5
    3.65909
    3.19601
    3.16246
    3.16228
    3.16228
    sqrt(10) = 3.16228
    Input a number:
    0

Сборка и использование компилятора

Исходные текста проекта (архивы в tar-gzip формате и в zip-формате):
"SimpComp.tgz"
"SimpComp.zip"

Для компиляции проекта следует выполнить команду

    make
в Unix'е или команду
    nmake -f Makefile.win
в операционной системе MS Windows. В последнем случае предполагается, что на компьютере установлена программа "Visual Studio 2008" (компонент C++) или ее более новая версия. Нужно в меню программ выбрать пункт
    Microsoft Visual Studio 2008 ->
        Visual Studio Tools ->
            Visual Studio 2008 Command Prompt
Далее, в открывшемся окне консоли надо переместиться в директорию с исходными текстами проекта, например, "С:\MyProjects\SimpComp":
    c:
    cd \MyProjects\SimpComp
и выполнить команду
    nmake -f Makefile.win
После этого в случае успешной компиляции в текущей директории будет создана программа компилятора "simpcomp.exe".

Для запуска компилятора в Unix'е следует исполнить команду

    ./simpcomp
под MS Wndows — команду
    .\simpcomp
или просто
    simpcomp
При этом компилятор читает текст программы из входного потока (с клавиатуры). Для окончания ввода программы следует набрать строку
    end
(можно ввести также символ конца файла — Ctrl+D в Unix'е или Ctrl+Z в MS Windows). После этого программа выполняется, ее ввод и вывод происходит в консоли.

Можно также выполнить программу, предварительно записанную в файле. Например, для исполнения программы из файла "square20.s" (этот файл содержится в проекте) в Unix'е следует выполнить команду

    ./simpcomp square20.s

или под MS Windows — команду
    simpcomp square20.s

Структура компилятора

Компилятор состоит из следующих модулей и файлов:

Все файлы запакованы в единый tgz-архив "SimpComp.tgz" (есть и файл "SimpComp.zip" для тех, кто предпочитает zip-архивы). При распаковке архива создается поддиректория "SimpComp" в текущей директории, куда и помещаются все распакованные файлы.