Использование учебного компилятора в курсе "Формальные языки и основы компиляции" Why Tiny-C. Курс основ компиляции, имеющий практическую направленность, должен включать в себя значительное количество разобранных при- меров. Весьма привлекательно привести в качестве крупного при- мера законченный компилятор. Он может служить источником боль- шого количества задач на модификацию и усовершенствование ре- ального, достаточно большого проекта. Выбор компилятора для та- кой цели (начиная с выбора входного и выходного языка) не явля- ется тривиальной задачей. Компилятор реального языка программи- рования в коды реального процессора - слишком большая для учеб- ных целей программа. Она окажется неизбежно перегруженной дета- лями, связанными с особенностями языка, системы команд, опера- ционной системы и т.д. С другой стороны, компилятор "игрушечно- го" языка в коды виртуальной машины обычно не содержит многих важных компонент настоящего компилятора. Такой "игрушечный" пример не позволяет продемонстрировать проблемы, возникающие, например, при организации таблиц символов или генерации кода. В качестве компромиссного решения был выбран написанный спе- циально для учебных целей компилятор с подмножества языка C в язык ассемблера машины PDP11. Реализованное подмножество C близко языку, который воспринимает компилятор Small-C [1]. Пар- сер этого компилятора реализован методом рекурсивного спуска. В процессе разбора немедлeнно порождается выходной текст на ас- семблере процессора i8080. Реализация компилятора содержит око- ло 2000 строк на собственном входном языке, таким образом, выб- ранное подмножество C достаточно богато. Познакомиться с этим компилятором можно в более доступной книге [2]. Приведенный там текст компилятора RatC на 90% совпадает с текстом Small-C. Исходный компилятор не подходил для наших целей по следующим причинам: 1. Этот весьма компактный компилятор написан "одним куском" и плохо приспособлен для изучения и модификации. 2. Генерация кода для выражения сводится, фактически, к пе- реводу его в постфиксную запись и реализации "стекового кальку- лятора" на аппаратном стеке микропроцессора. В результате, был написан компилятор, названный TinyC. Из [1] был позаимствован только исходный язык. Для реализации ска- нера и парсера компилятора применены LEX и YACC соответственно, что позволило сделать компилятор весьма компактным и обозримым. Некоторое влияние на структуру компилятора оказал Portable C Compiler операционной системы UNIX. Описание языка Small-C является подмножеством C. С целью дать представление об этом подмножестве здесь лишь перечислены элементы C, вклю- ченные в (или исключенные из) этого подмножества. Более подроб- ные сведения о языке C можно почерпнуть в [3]. Единицей компиляции является файл - возможности сборки прог- раммы из нескольких файлов и раздельной компиляции обеспечива- ются соответствующими ассемблером и загрузчиком. Средства препроцессора обеспечивают только простую (без па- раметров) подстановку имен, определяемых оператором #define. Можно описывать переменные только следующих типов: целые (int), символы (char), одномерные массивы символов или целых, указатели на символы или целые. Отсутствует инициализация переменных. Имеется только два класса памяти: статическая, доступная во всей программе (если переменная описана вне функции), и автома- тическая, локализованная в пределах функции (если имя описано внутри функции). Нельзя специфицировать тип результата функции - всегда возвращается целое. Однако язык позволяет свободно смешивать указатели и целые. Из управляющих конструкций реализованы условный оператор if, if else, цикл пока while, а также операторы break, continue, return и return. В выражениях можно употреблять только следующие операции, перечисленные в порядке убывания приоритета: [] (индексация) () (вызов функции) ++ -- (префиксные и постфиксные) * (значение) & (адрес) - (унарный минус) * / % + - << >> < <= > >= == != & ^ | = Таким образом, не реализованы операции языка C: ? : || && sizeof (имя типа) ~ ! -> . , Не производится свертка константных выражений - размерность массива должна быть константой. Для формальных параметров она может опускаться, например int a[]; Архитектура компилятора Компилятор построен по классической однопроходной схеме. Он содержит следующие исполнители: Сканер, Парсер, Таблица симво- лов, Дерево, Генератор кода. В начале работы открываются вход- ной и выходной файлы, выполняется инициализация и запускается парсер - все остальное делают его семантические подпрограммы. Для получения очередной лексемы парсер вызывает сканер. Встре- ченные в исходном тексте имена переменных заносятся сканером в таблицу символов. Для всех элементов языка кроме выражений (т.е. для описаний переменных, функций, управляющих конструк- ций) выходной ассемблерный код порождается немедленно в процес- се их разбора. В процессе разбора выражения парсер строит его дерево. Построение осуществляется с помощью исполнителя "Дере- во", который также проверяет соответствие типов операндов и вы- числяет тип каждого узла дерева. После того, как выражение ра- зобрано целиком, вызывается генератор кода, строящий код для выражения по его дереву. Генерация кода состоит из двух этапов. Вначале вычисляется "сложность" (количество регистров, необхо- димое для вычисления) каждого поддерева. На втором этапе в про- цессе обхода дерева генерируется собственно код. Рассмотрим каждый из исполнителей более подробно. Сканер (файл scaner.l) Задача, решаемая сканером, довольна проста - выделение лек- сем из последовательности символов входного файла. Сканер ре- ализован с помощью программы LEX. Конечный автомат может нахо- дится в следующих метасостояниях: S - исходное (нормальное) состояние, COM - внутри комментария: поиск конца комментария "*/", STR - внутри строки, заключенной в кавычки или апострофы, DEF1, DEF2 - обработка предложения препроцессора #define. Распознавание лексемы завершается возвратом из функции yylex. При этом возвращается код лексемы, а переменной yylval присва- ивается ее значение. Для уменьшения объема парсера многие син- таксически эквивалентные лексемы имеют однаковый код, различа- ясь лишь значениями. Например, ключевые слова int и char соот- ветствуют одной лексеме TYPE, но имеют различные значения - INT и CHAR соответственно. Значениями операторов + * и т.д. являют- ся сами коды лексем. Выделенный сканером идентификатор немедленно заносится в таблицу символов с помощью функции nameenter. Возвращаемое этой функцией значение лексемы NAME является указателем на запись в таблице символов, соответствующую данному идентификатору. Обра- тите внимание на то, что для правильного распознавания ключевых слов регулярное выражение для идентификатора должно быть распо- ложено после выражений, описывающих ключевые слова. Выделение обработки комментария в отдельное состояние поз- воляет обрабатывать комментарии любой длины, не ограниченной размером буфера сканера yytext. Обработка текстовых констант производится в отдельном состо- янии. Символы строки, включая преобразованные последовательнос- ти вида \x или \nnn, накапливаются в буфере strbuf. После раз- бора символьный константы (в одиночных кавычках) ей присваива- ется код ICON (целая константа) и возвращается целое значение. Строчная константа (в двойных кавычках) немедленно заносится в секцию строчных констант с помощью функции strenter. Возвраща- емое этой функцией число - значение лексемы STRING - номер мет- ки, присвоенной строке. Функции простейшего препроцессора реализованы непосредствен- но в сканере. При разборе строки вида # define <имя> ..., имя (точнее не более NAMLEN первых его символов) копируется в буфер defname. Определение имени копируется в буфер strbuf, при этом из него удаляются лишние пробелы, табуляции и комментарии. За- тем имя и его определение заносится в таблицу функцией defenter. После выделения каждого имени проверяется, не содержится ли его определение в таблице макро. Если определение найдено (в этом случае функция deflookup возвращает указатель на значе- ние), то значение возвращается в входной поток с помощью функции unput, а само имя игнорируется. Такой трюк обеспечивает повторное сканирование значения макро. Построенный с помощью LEX'а сканер считывает исходный файл из потока yyin и содержит в переменной yylineno номер текущей строки, который используется компилятором при выдаче сообщений об ошибках. Упражнения: 1. Что произойдет при сканировании следующего оператора? #define four 2 /* комментарий 1 */ * /* комментарий 2 */ 2 Исправьте эту ошибку. 2. Если определение макро содержит прямую или косвенную ре- курсию, то сканер зациклится при попытке его расширения. (Из-за ограниченного размера стека возвращенных символов этот цикл быстро завершится терминацией компилятора с диагностикой типа memory protect violation). Сделайте диагностику таких ошибок более разумной. 3. Добавьте в препроцессор оператор #include "имя файла". Для решения этой задачи будет полезна функция yywrap LEX'a. 4. Добавьте в препроцессор операторы условной компиляции: #ifdef <имя>, #else, #endif. 5. Сделайте так, чтобы текст после символов // до конца строки рассматривался как комментарий. (Такой способ записи комментария имеется в языке C++.) 6. Обеспечьте обработку вложенных комментариев, т.е. выраже- ние /* /* */ */ должно восприниматься как один комментарий. 7. Сканер TinyC воспринимает только десятичные константы. Обеспечьте возможность записи восьмеричных и шестнадцатеричных констант как в "настоящем" C. 8. Добавьте в язык оператор asm { последовательность команд ассемблера } для вставки в C-программу фрагментов на ассембле- ре. 9. Разделители строк в C игнорируются везде кроме оператора препроцессора #define и строчных констант. Чтобы игнорировать их и в этих случаях надо в конце строки записать "\". Добавьте эту возможность в TinyC. 10*. Реализуйте сканер, не используя LEX. Насколько изменился объем сканера в строчках исходного текста и в байтах скомпили- рованной программы? Насколько быстрее он стал работать? Парсер (файл parser.y) Парсер реализован с использованием программы YACC. Ес- тественная грамматика языка TinyC оказалась подходящей для раз- бора методом LALR(1). (Она содержит один конфликт в случае if(e)if(e)s_else s, который правильно разрешается по умолчанию предпочтением сдвига свертке). Она была дополнена правилами для восстановления после синтаксических ошибок и преобразована в правилах, описывающих условный оператор, так, чтобы метку для перехода вперед можно было хранить в семантическом стеке. Семантический стек состоит из элементов типа YYSTYPE, опре- деленного как int для pdp11. Этот тип должен быть целым размера достаточного для хранения указателя, так как в стеке и перемен- ной yylval могут хранится целые числа, указатели на элемент таблицы символов и на узел дерева выражений. Средства YACC'a для спецификации типа терминалов и нетерминалов (и объявление типа YYSTYPE как union) не использовались, так как некоторые компиляторы (например, decus C) не умеют присваивать структуры. Описания переменных компилируются непосредственно. Для гло- бальной переменной генерируется метка с ее именем и резервиру- ется память. При описании локальной переменной ей отводится место в кадре стека текущей функции, в результате чего locptr становится равным смещению переменной относительно fp. Смещение формального параметра определяется при его появлении в списке. Описание формального параметра не обязательно, по умолчанию он имеет тип int. Описание формального параметра - массива преоб- разуется в описание указателя. Порождаемый код для параметров и локальных переменных состоит из комментария. Соглашения о связях компилятора TinyC совпадают с соглашени- ями, принятыми в компиляторе decus C, что позволяет использо- вать его библиотеки при выполнения программ. Параметры функции передаются через стек, первым в стек проталкивается самый пра- вый параметр. Функция вызывается командой jsr pc, при выполне- нии которой в стек помещается адрес возврата. В начале каждой функции командой jsr r5, csv~ вызывается подпрограмма, сохраня- ющая регистры и устанавливающая новое значение fp (frame pointer, указатель кадра), в качестве которого используется ре- гистр r5. После этого на стеке отводится место для локальных переменных. Функция обязана восстановить содержимое регистров r2-r5 при возврате. Это осуществляется переходом jmp cret~ на соответствующую подпрограмму. Кадр стека функции имеет имеет следующую структуру: ... 1 параметр 4 = SPARAM адрес возврата 2 r5 (старый fp) 0 <- fp r4 -2 r3 -4 r2 -6 1 лок. переменная -8 = SAUTO = sp после возврата из csv~ ... Порожденный компилятором код размещается в трех секциях. В секции c~code содержаться выполняемые команды процессора, в секции c~data - глобальные (статические) переменные и в секции c~strn - строчные константы. Так как описания глобальных пере- менных могут появляться только между функциями, то для правиль- ного размещения кода достаточно в начале каждой функции делать текущей секцию c~code, а в конце - c~data. При появлении строч- ной константы в тексте, сканер вызывает предписание strenter, которое делает текущей секцию c~strn, печатает константу и де- лает текущей снова секцию c~code. (Строчные константы могут по- являться только внутри функций). Для получения уникальных меток служит макро newlab, выдающее при каждом новом обращении очередное натуральное число. В конце каждой функции имеется метка, содержащаяся в переменной retlab. Оператор return компилируется в переход на эту метку. Управляющие конструкции компилируются обычным образом: e e labcont: if( e ) bxx lab1 if( e ) bxx lab1 e stmt ... stmt ... while( e ) bxx labbrk lab1: br lab2 stmt ... else lab1: br labcont stmt ... labbrk: lab2: Для хранения меток условного оператора используется семантичес- кий стек парсера: lab1 приписывается к нетерминалу ifh, lab2 - к нетерминалу ihelh. Для хранения меток операторов цикла ис- пользуется отдельный стек wstack, указатель wptr указывает на вершину этого стека. Это решение связано с тем, что метки цикла используются операторами break и continue, при обработке кото- рых нетерминалы цикла могут оказаться на разной глубине семан- тического стека. В стеке wstack хранится только метка labcont (на нее осуществляется переход по оператору continue). Так как эта пара меток получена двумя последовательными вызовами newlab, метка labbrk (на нее переходят по оператору break) всегда равна labcont+1. В процессе разбора выражения строится его дерево. Нетермина- лам e (выражение), term, elist (список фактических параметров), elist2 приписывается семантическая информация - указатель на корень дерева, описывающего соответствующее нетерминалу выраже- ние. При свертке правила грамматики вызывается функция tree, которая добавляет к дереву новый корень для унарных операций или объединяет два дерева в одно для бинарных операций. При создании листьев дерева, которые соответствуют именам перемен- ных и целым и строчным константам, их значения, занесенные ска- нером в переменную yylval, передаются исполнителю "дерево" че- рез переменные trnval (указатель на элемент таблицы имен) или trival (значение целой константы или номер метки строчной константы). Операция индексации массива a[i] при построении дерева пере- водится в форму *(a+i). Коды унарных операций - * & получаются из кодов лексем совпадающих с ними бинарных операций прибавле- нием константы UNARY, а коды префиксных операций ++ -- - из ко- дов постфиксных операций прибавлением константы PREFIX. Опера- ция "," имеет семантику "протолкнуть результат вычисления лево- го поддерева в стек параметров". Для правильного порядка вычис- ления фактических параметров нетерминал elist2 (непустой список параметров) задан с помощью правой рекурсии. При свертке правила, содержащего выражение в правой части, вызывается генератор кода для выражения. В качестве параметра ему передается элемент семантического стека, содержащий указа- тель на построенное к данному моменту дерево выражения. При синтаксической ошибке парсер, построенный с помощью YACC'а выдает только малоинформативную диагностику "Syntax error in line ...". Восстановление после ошибки происходит сле- дующим образом. Если ошибка обнаружена внутри оператора, то пропускаются все лексемы до ближайшей точки с запятой или зак- рывающейся фигурной скобки. Фрагмент текста от начала операто- ра, при разборе которого была обнаружена ошибка, до точки запя- той или скобки, включая последние, сворачивается в нетерминал stmt (оператор). Если ошибка обнаружена вне оператора, то пар- сер "вываливается" на внешний уровень, пытаясь начать разбор описания глобальной переменной или функции. Упражнения: 1. Добавьте в язык возможность инициализации глобальных пе- ременных целыми константами. int i = 3; char text[4] = { 'd', 'a', 't', 'a' } ; 2. Добавьте в язык альтернативный синтаксис описания фор- мальных параметров функции: strncpy( d, s, n ) char *d, *s; int n; strcpy( char *d, *s; int n ) 3. Добавьте в язык цикл DO stmt WHILE( e ) CM. 4. Добавьте в язык цикл for. Обратите внимание на правильную работу операторов break и continue. 5. Добавьте в язык метки и оператор goto. 6. Добавьте в язык оператор switch. 0*. Исследуйте эффективность реализованных средств восста- новления после ошибок. Проверьте по-крайней мере поведение ком- пилятора при типичных ошибках - несбалансированных круглых или фигурных скобках. Усовершенствуйте восстановление после ошибок и сделайте их диагностику более информативной. Таблица символов (файл symtab.c) Исполнитель "таблица символов" содержит реализацию двух таб- лиц - таблицы обозначений, введенных оператором #define и таб- лицы имен C-программы. Элемент таблицы #define, описываемый структурой dnd, имеет переменный размер и состоит из указателя на следующий, поля имени обозначения фиксированной длины и поля определения пере- менной длины. Обе строки терминированы нулем. Таблица состоит из DEFMAX односвязных списков. Указатели на списки собраны в массив defhdr. Определение добавляется/ищется в списке с индек- сом равным значению hash-функции, принимающей значения в диапа- зоне 0..DEFMAX-1. В качестве такой функции выбрана сумма симво- лов имени по модулю DEFMAX. Таблица имен C-программы имеет несколько более сложную структуру, так как должна обеспечивать хранение локальных для каждой функции и глобальных переменных. Записи всех переменных провязаны в один односвязный список, указатель на который хра- нится в переменной stbhdr. Так как указатель на следующий эле- мент списка stnext является первым в среди полей записи, то stbhdr может выполнять роль NULL-элемента, что делает операцию удаления первого элемента списка однородной. Локальные переменные текущей функции находятся в начале это- го списка, раньше всех глобальных переменных. Для их разделения используется указатель stbloc, ссыающийся на описание первой в списке глобальной переменной. В начале каждой функции парсер вызывает функцию openlocal, устанавливающую этот указатель на первый элемент списка. В конце каждой функции парсер вызывает функцию closelocal, которая удаляет из списка все локальные пе- ременные, занесенные сканером в процессе разбора, и устанавли- вает указатель stbloc в конец списка. Это обеспечивает правиль- ное разделение локальных и глобальных переменных, так как впер- вые встреченная сканером переменная добавляется в начало спис- ка, а любая переменная в C должна быть описана до ее использо- вания. Исключение из этого правила составляют функции, т.е. пере- менная, впервые встреченная в контексте f( ... ), неявно описы- вается как функция. Поэтому в closelocal не удаляются из табли- цы переменные - функции. Так как используемый ассемблер требует обязательного описания всех имен (но не обязательно перед упот- реблением), в конце работы компилятора вызывается функция stogbl. В результате ее работы в директивах ассемблера .globl перечисляются все имена, находящиеся в данный момент в таблице символов. Эта директива объявляет глобальными уже определенные символы (т.е. имена глобальных переменных и функций) и внешними неопределенные имена функций (библиотечных или отдельно скомпи- лированных функций). Для получения/освобождения памяти для элементов таблицы сим- волов используется стандартные функции C-библиотеки malloc и free. Упражнения: 1. Реализуйте таблицу имен hash-методом. 2. Реализуйте таблицу имен в виде двоичного дерева. Изменилась ли общая эффективность компилятора? 3. Насколько хороша используемая в таблице #define hash-функция? Проведите эксперименты с другими функциями. 4. Реализуйте свою систему распределения динамической памя- ти. Она должна быть более простой и эффективнной чем стан- дартная, так как блоки переменной длины #define таблицы не ос- вобождаются, а освобождаемые блоки таблицы символов имеют фик- сированную длину. Дерево (файл tree.c) Память для дерева выражений выделяется из массива trebuf. Так как дерево удаляется целиком после генерации кода для него, то выделение памяти тривиально - указатель trefre указывает на первый свободный узел дерева. Функция tralloc возвращает указа- тель на новый узел дерева, занеся в него код операции и указа- тели на левое и правое поддерево. Функция tone, возвращает де- рево, соответствующее целой константе 1, функция trfree осво- бождает всю память, занимаемую деревом. Основная функция tree аналогична tralloc, но кроме добавле- ния еще одного узла к дереву, она проверяет соответствие типов операндов для данной операции и вычисляет тип результата. Этот тип определяется комбинацией битов поля trtag: бит 1 0 ------------------------------------------- TLVAL l-значение значение TPTR указатель переменная TCHAR char int Кроме вычисления типа результата, функция определяет, ис- пользуется ли аргументы в качестве значения или l-значения. Для упрощения программы проверки и тип результата закодиро- ваны в векторе trops, индексируемым кодами операций. По умолча- нию тип результата равен типу левого операнда. Биты: OLV - левый аргумент используется как значение. Если этот бит равен нулю, то левый аргумент должен быть l-значением, а тип результата равен типу левого аргумен- та, но не является l-значением. Если тип левого аргумента - char, то тип результата будет int. ORV - правый аргумент используется как значение. OLI - левый аргумент не должен быть указателем ORI - правый аргумент не должен быть указателем OINT - результат операции целый OCMP - операция отношения: если хотя бы один из операндов ука- затель, то требуется беззнаковое сравнение OSPC - предыдущих проверок недостаточно, код обрабатывается отдельно оператором switch. Специальные действия требуются для всех листьев дерева - оп- ределяется их тип, а в в поле trval заносится их значение (но- мер метки строки, ссылка в таблицу символов для имен). Проверя- ется, что используемая в выражении переменная описана. Для операций сложения и вычитания отдельно обрабатываются случаи (указатель +- целое) и (указатель-указатель). Для ука- зателей на целое в дерево вставляется операция сдвига, реали- зующая умножение/деление на 2 (размер целой переменной в бай- тах). Специальные действия требуются также и для унарных операций & и *, преобразующих <тип> в указатель на <тип> и наоборот. Запрещение применения операции & к указателю позволяет обойтись конечным числом типов для промежуточных результатов. Упражнения: 1. Добавьте в компилятор вычисление простейших константных выражений: "а=2+2;" должно компилироваться в "mov $4,a". 2. Введите в язык унарные операции "!" и "~". 3. Добавьте в язык операцию ",". 4. Добавьте в язык операцию ? :. 5. Добавьте в язык операцию sizeof( тип ) sizeof( переменная ) 6. Добавьте в язык операции || и &&. Генератор кода (файл expr.c) Имеется несколько функций, генерирующих код для выражения, заданного деревом, указатель на которое передается им в ка- честве аргумента: aeval - поместить результат выражения в r0 ( RETURN e CM ) eeval - результат не используется ( е CM ) leval - результат в кодах в CC. ( if( e ) ... ) После небольшого изменения корня дерева каждая из этих функций обращается к общей функции geval. Перед непосредственной генерацией кода функция pregen опре- деляет количество регистров, необходимое для вычисления каждого поддерева. Предполагается, что все промежуточные результаты хранятся в регистрах. Такая разметка дерева поможет затем поро- дить более оптимальный по число используемых регистров код, ис- пользуя весьма простую идею, известную как алгоритм Сети-Ульма- на. Он заключается в том, что при вычислении выражения, задан- ного деревом, следует вначале вычислять более дорогое поддере- во. Функция pregen вычисляет стоимость каждого поддерева обходя дерево снизу вверх. Это число, помещаемое в поле trcost каждого узла, является функцией от самого узла и стоимостей его прямых потомков. Для упрощения функции pregen (так же как и в функции функции tree) используется вектор trops, индексируемый кодом операции. Биты, выделяемые маской CMSK, задают одну из 8 функций, встречающихся для различных операций. Естественно, не- которые операции не подходят ни под одно из этих правил. Такие операцие помечены битом CSPC и обрабатываются отдельно. Генерация кода для выражения осуществляется в функции gen, также в процессе обхода дерева снизу вверх. Вместе с генерацией кода для узла дерева, функция gen заносит в поля trmode и trreg местоположение результата операции. В зависимости от значения trmode результат операции находится: REG - в регистре trreg IREG - в памяти косвенно по адресу в регистре trreg IMMED - непосредственное значение, описываемое полем trval MEM - в памяти по адресу, задаваемым полем trval LMEM - на стеке,смещение относительно fp определяется trval IMEM - то же, что и MEM, плюс еще одна косвенность ILMEM - то же, что и LMEM, плюс еще одна косвенность CC - в кодах условия, trreg содержит индекс команды ус- ловного перехода для проверки: ne eq ge gt le lt his hi los lo Некоторые биты из поля trtag, используются для генерации ко- да: NOUSE - значение, вырабатываемое операцией, не используется; в случаях "a=b; f(); i++; i--;" можно порождать более эффектив- ный код, чем в случаях "c=a=b; c=f(); c=i++; c=i--;". Для опе- раций сравнения этот бит означает, что результат операции дол- жен быть оставлен в кодах условий, а не преобразован в целое число, равное 0 или 1. TUNSIGN - операнды операции сравнения - беззнаковые числа, иначе они рассматриваются как целые со знаком. Этим битом также помечена операция сдвига для масштабирования результата вычита- ния двух целых указателей. Содержимое поля trval определяется комбинацией следующих двух битов: TNAME - trval.n указывает на элемент таблицы символов, TSTRING - trval.i содержит номер метки строчной константы, иначе (оба бита равны нулю) trval.i - содержит значение це- лой константы. Перед генерацией кода данной операции, порождается код для ее левого и правого операндов, рекурсивно вызывая функцию gen. (Вначале порождается код для более сложного поддерева). В ре- зультате порожденного кода будут вычислены операнды, а информа- ция о том, где они находятся, будет занесена в соответствующие им узлы дерева. Собственно генерация кода выполняется интерпре- татором - функцией o. Аргумент этой функции - текстовая строка, содержащая как обычные символы, так и интерпретируемые команды - они закодированы большими латинскими буквами. Обычные символы просто копируются в выходной файл, начиная с новой строки и та- булятора при каждом вызове интерпретатора. С каждой командой связано определенное действие интерпретатора, например, подста- новка вместо него текстового представления аргумента. Такой прием позволяет сократить объем кодогенератора и сделать его более понятным по сравнению с разбором всех частных случаев ге- нерации кода с помощью многочисленных if и else. Для вычисления многих операций и хранения промежуточных ре- зультатов используются регистры общего назначения r0-r4, ре- гистр r5 используется в качестве указателя кадра стека. Инфор- мация о незанятых регистрах хранится в массиве regs. Если ре- гистр i свободен, то regs[i] содержит NULL. Для получения сво- бодного регистра служит функция regalloc, для освобождения - regfree. Для получения для команды MUL нечетного регистра, а для команды DIV - пары регистров, служит переменная rreq. Ре- гистр, используемый для хранения промежуточного результата, ос- вобождается при использовании этого результата, если его новое содержимое не является результатом выполняемой операции. Если выражение оказалось слишком сложным и для промежуточных резуль- татов не хватило регистров, то кодогенератор выдают отказ. Так как регистры r0 и r1 не сохраняются при вызове функции, при вы- делении регистра отдается предпочтение регистру с большим номе- ром. Упражнения: 1. Выражения "a>0" и "а=0" компилируются в команды "cmp a, $0" и "mov $0, a" соответственно. Сделайте так, чтобы в этом случае генерились более короткие и быстрые команды "tst a" и "clr a" соответсвенно. 2. Используйте для увеличения/уменьшения операнда на 1 ко- манды inc, dec. 3. Операция ip++ для целого указателя компилируется в две команды "inc ip". Сделайте код для этого случая более опти- мальным. 4. Устраните лишнюю операцию пересылки в коде для "return a+1" - "mov a, r4; add $1, r4; mov r4, r0" 5. Обеспечьте генерацию кода для выражения любой сложности. Один из способов: при отсутствии свободного регистра освободите один (или несколько) регистров, занятых промежуточными резуль- татами, сохранив их значения в стеке. Когда сохраненные в стеке результаты потребуются, их можно будет возвратить обратно в ре- гистры или использовать непосредственно в команде с режимом ад- ресации (sp)+. Для правильной работы этого алгоритма следует первыми сохранять результаты, находящиеся ближе к корню дерева. 6. Обеспечьте более эффективный код при обращению к масси- вам: "a[1]=i" может быть скомпилировано в "mov i, a+2", если a описан как статический массив целых. Запускач и средства отладки (tinyc.c и debug.c) В rt-11 компилятор запускается так: run tc argv: [-s] [-t] [filnam] Ключи вызывают вывод на терминал отладочных печатей: s - со- держимого локальной таблицы символов по окончанию каждой функции и глобальной таблицы символов в конце работы; t - пе- чать дерева для каждого выражения. Если входной файл имеет рас- ширение .C, то его можно не указывать. Выходной файл имеет то же имя, что и входной, но расширение .S. Если входной файл не указан, то исходная программа будет вводится из stdin, а ре- зультат компиляции выводится на stdout. Русско-английский бестолковый словарик К сожалению, большинство компиляторов не допускает кириллицы в именах переменных, поэтому в качестве имен переменных и констант использованы идентификаторы с англоязычной мнемоникой. Здесь сделана попытка расшифровать некоторые из них. addname addstr aeval AND /* & */ ASG /* = */ BREAK /* break */ C0 /* 0 */ C00 /* CL==0 && CR==0 ? 0 : CSU */ C01 /* CL==0 && CR==1 ? 2 : CSU */ C10 /* CL==1 && CR==0 ? 2 : CSU */ CC /* cc */ CCMP /* Операции сравнения */ ccs CDIV /* Для div и mod */ CHAR chkgarb CL closelocal CM /* ; */ CMAX /* max( CL, CR ) */ CMSK COM CONTINUE/* continue */ cpl CR CSPC /* Требуется отдельная обработка */ CSU /* CL==CR ? CL+1 : max( CL, CR ) */ CSU2 /* max( CSU, 2 ) */ cv data_def dcl dcldim dclfnct dclprm dcls dcltype dclvar DEBUG При определении этого символа в текст компилятора включаются отладочные средства. debugs debugt DECR DEF1 DEF2 def defenter defhdr deflookup DEFMAX Размер hash-таблицы для #define defname defs deref dhash DIGIT dim DIV DIVOP /* DIV MOD */ dnd dnname dnnext dnval e ea eeval efnct elist slist2 ELSE /* else */ EQ EQUOP /* EQ NE */ ERRMAX Прекращение работы после ERRMAX ошибок error esize FCALL fatal filnam fnct_def free GE gen geval GT i2oct ICON /* Целое 123, 'а' */ ICOP /* ICR DECR */ ICR IDENT IF /* if */ ifelh ifh ILL IMEM /* *name */ IMMED /* &name */ ILMEM /* *name(fp) */ IREG /* (reg) */ INT LB /* [ */ LC /* { */ ldefs LE LETTER leval LMEM /* name(fp) */ LOAD loadreg loclab locptr LP /* ( */ LS LT malloc MEM /* name */ MINUS /* - */ MOD MUL /* * */ NAME /* Имя */ nameenter NAMLEN Значимы только первые NAMLEN символов NE nerr nest newlab NOLETTER NREGS ntags NUMBER o obra obxx OCMP /* Если надо, то беззнак. сравнение */ OINT /* Результат - целое */ olab OLI /* Левый аргумент - не указатель */ OLV /* Левый аргумент - значение */ oparam openlocal oprolog OR /* | */ ORI /* Правый аргумент - не указатель */ ORV /* Правый аргумент - значение */ OSPC /* Требуется специальная обработка */ ot pars pars1 parser.c parser.h parser.y pdefs PLUS /* + */ PREDECR PREFIX pregen PREICR ref REG /* reg */ regalloc regfree regs retlab RB /* ] */ RC /* } */ RELOP /* LT LE GT GE */ RETURN /* return */ RP /* ) */ rreq RRORD RRODD RRPAIR RS S SAUTO Смещение первой локальной переменной scaner.c scaner.h scaner.l SHIFTOP /* LS RS */ SM /* ; */ SPARAM Смещение параметров относительно fp STACHAR /* массив символов */ STAINT /* массив целых */ stb stbhdr stbloc STCHAR /* символ */ stdio.h stinit STINT /* целое */ STFUNC /* функция, возвращающая целое */ STLOCAL /* локальная (auto) переменная */ stlonly STMASK /* маска для выделения типа */ stmt stmts stname stnext stogbl STPARAM /* не описанный явно форм. параметр */ STPCHAR /* указатель на символ */ STPINT /* указатель на целое */ STR stradd strbuf strcmp strcpy strend strenter streq STRING /* Строка "abc" */ STRMAX Максимальная длина строки или #define strptr stsize sttype STUNDEF /* не определено */ term TEST TCHAR /* Char (Int otherwise) */ tinyc.h TLVAL /* Lvalue (Value otherwise) */ TNAME /* Значение - имя */ TNOUSE /* Результат операции не используется*/ tone TPTR /* Pointer (Variable otherwise) */ tralloc trcost tre trebuf tree trefre TREMAX Макс. число узлов в дереве выражений trfree tright trival trleft trmode trnval trop trops[]; /* Индексируется кодом операции */ trreg trtag trval TSTRING /* Значение - строка */ TUNSIGN /* Сравнение беззнаковое */ TYPE /* INT CHAR */ UMINUS UAND UMUL UNARY WHILE /* while */ WHISP WHISP1 WMAX Максимальная вложенность while wptr wstack XOR /* ^ */ yyerror yyin yylineno yylval yyout YYSTYPE yytext yywrap Литература 1. Ron Cain. A Small C compiler for the 8080's. Dr. Dobbs Journal, April-May,1981. 2. Р.Берри, Б.Микинз. Язык СИ: Введение для программистов. - М.:Финансы и статистика,1988. 3. Б.Керниган, Д.Ритчи. Язык программирования Си. - М.:Фи- нансы и статистика, 1985. 4. UNIX: PCC, LEX, YACC 5. Decus C 6. Д.Хендрикс. Компилятор языка Си для микроЭВМ. М.:Радио и связь, 1989.