Deitel C - Задачи - Глава 12+
Группы и результаты
8 А | 8 Б | 8 В | 8 Г |
SIMPLE
В этом упражнении мы напишем компилятор, который преобразует программу на высокоуровневом языке в машинные коды SML, которые загруженные в Simpletron могут быть выполнены.
Сначала определим наш высокоуровневый язык Simple. Он будет похож на ранние версии языка Basic.
Каждое выражение языка Simple состоит из номера строки и инструкции языка.
Номера строк должны следовать в возрастающем порядке.
Каждая из инструкций начинается с одной из команд:
- rem
- input
- let
- goto
- if ... goto
- end
Все команды кроме end могут быть использованы многократно.
Simple оперирует только целыми числами и имеет только 4 оператора: +, -, *, /.
Эти операторы имеют такой же приоритет как и в языке Си. В выражениях могут быть использованы скобки.
- rem - команда для ввода комментариев в коде. Не компилируется в машинный код.
- input - команда для ввода числа от пользователя. Выводит символ вопроса, чтобы показать что ожидается ввод. В качестве параметра ожидает имя переменной.
- let - присваивает переменной результат вычисления выражения
- print - выводит значение переменной
- goto - переводит программу к исполнению строки программы указанной в параметре
- if ... goto - переводит программу к исполнению строки программы указанной в параметре в случае выполнения условия
Язык Simple распознаёт только строчные (маленькие) буквы.
Имя переменной может быть только из одной буквы.
Все переменные в Simple целые числа.
Simple не требует объявления переменных. Появление переменной в программе объявляет и инициализирует её нулём.
В условных операторах допускаются операторы сравнения <, >, <=, >=, ==, !=.
Сравнивать можно только 2 переменные или переменную с константой. Оценка выражений не поддерживается.
Рассмотрим несколько программа на языке Simple.
Сумма 2 чисел
10 rem determine and print the sum of two integers 15 rem 20 rem input the two integers 30 input a 40 input b 45 rem 50 rem add integers and store result in c 60 let c = a + b 65 rem 70 rem print the result 80 print c 90 rem terminate program execution 99 end
Определение большего из 2-х чисел
10 rem determine the larger of two integers 20 input s 30 input t 32 rem 35 rem test if s >= t 40 if s >= t goto 90 45 rem 50 rem t is greater than s, so print t 60 print t 70 goto 99 75 rem 80 rem s is greater than or equal to t, so print s 90 print s 99 end
В Simple нет циклов, но он может эмулировать такое поведение через операторы if ... goto.
Например следующая программа рассчитывает квадрат вводимых чисел пока не будет введено число -9999.
10 rem calculate the squares of several integers 20 input j 23 rem 25 rem test for sentinel value 30 if j == -9999 goto 99 33 rem 35 rem calculate square of j and assign result to k 40 let k = j * j 50 print k 53 rem 55 rem loop to get next j 60 goto 20 99 end
Компилятор
Программа на языке Simple сохраняется в файл. Компилятор на его основе создаёт файл с инструкциями SML по одной на строке.
Затем файл загружается в симулятор Simpletron'а. Симулятор должен быть модифицирован, чтобы загружать инструкции не с клавиатуры, а из файла при запуске программы.
Компилятор выполняет 2 прохода Simple программы для преобразования её в SML-инструкции.
В первый проход создаётся таблица символов в которой хранятся каждая строка программы, все имена переменных и константы с типом и адресом памяти в итоговом наборе инструкций SML.
Первый проход также переводит команды языка в инструкции SML.
Если программа имеет оператор перевода контроля на более позднюю строку программы первый проход не сможет заполнить адрес перехода и оставит инструкцию неполной.
Второй проход дополняет эти инструкции и выводит результат в файл.
Первый проход
Изначально программа считывает строки программы из файла.
Затем строка разбивается (с помощью функции strtok) на части (token/токены) для обработки и компиляции.
Каждое выражение языка Simple начинается с номера строки и продолжается командой.
Если мы находим переменную или константу, то мы помещаем их в нашу таблицу символов.
Номер строки помещается в таблицу символов только если он первый токен в строке.
Таблица символов - это массив структур описывающих символы программы.
struct tableEntry { int symbol; char type; /* 'C', 'L' or 'V' */ int location; /* 00 to 99 */ };
Каждый элемент таблицы символов содержит 3 элемента:
C | L | V | |
---|---|---|---|
int symbol | значение константы | номер команды Simple | целое число, являющееся ASCII - кодом символа представляющего имя переменной |
char type | С - константа/constant | L - номер строки/line number | V - переменная/variable |
int location | адрес константы в итоговой программе SML | адрес SML-инструкции соответствующей команде строки Simple | SML-адрес хранения переменной |
Память для переменных и констант выделяется в памяти Simpletron'а с конца.
Команды Simple соответствуют инструкциям SML, например:
- input(Simple) - 10(Read SML)
- print(Simple) - 11(Write SML)
В случае команды чтения необходимо указать в какую ячейку памяти необходимо записать введённое пользователем значение.
Компилятор ищет в таблице символов элемент указывающий, на адрес памяти соответствующей переменной.
Компиляция строки зависит от команды. Например если компилятор встречает команду rem остальная часть строки игнорируется.
Команды input, print, goto и end переводятся напрямую в инструкции read, write, branch и halt. (Команда goto может содержать ссылку на недоступный ещё в таблице будущий элемент, который будет заполнен при втором проходе компилятора)
При неполной компиляции инструкция должна быть помечена для второго прохода компилятора. Для этого используем массив целых чисел flags размером с память Simpletron'а и инициированные значением -1.
Если положение инструкции в памяти SML строки программы Simple ещё не известно номер строки сохраняется в массиве флагов под индексом соответствующим адресу неполной инструкции.
Операнд неполных инструкций временно выставляется равным нулю.
Компиляция условных переходов (if...goto) и команды let представляет большую сложность.
Компиляция условных переходов производится с помощью инструкций branch zero или branch negative или их комбинации.
Для команды let компилятор создаёт код для оценки арифметического выражения и/или констант.
В выражении операторы и операнды должны быть разделены пробелами.
Компилятор переводит инфиксную форму выражения в постфиксную и производит его оценку выражения, но вместо реальных расчётов генерирует соответствующие SML инструкции, вставляя адреса переменных из таблицы символов.
В стек при оценке вставляются адреса памяти вместо реальных значений.
В результате каждая часть выражения хранится во временной области памяти и возвращается в стек для продолжения оценки.
После завершения оценки в стеке остаётся адрес памяти где хранится результат вычислений.
Дальше генерируется SML инструкция присваивающая результат переменной из левой части команды let.
Второй проход
Во время второго прохода компилятор выполняет 2 задачи: разрешает неопределённые ссылки и выводит SML инструкции в файл.
Для всех элементов в таблице флагов (flags) не равных -1 найти в таблице символов нужный элемент с типом L - номер строки и дополнить инструкцию адресом SML.
После уточнения всех необходимых инструкций они выводятся в файл по одной инструкции на строку.
Пример: Программа вводящая число x и рассчитывающая сумму чисел от 1 до x
Команда Simple | Адрес SML + инструкция | Описание |
---|---|---|
5 rem sum 1 to x | none | rem ignored |
10 input x | 00 +1099 | read x into location 99 |
15 rem check y == x | none | rem ignored |
20 if y == x goto 60 | 01 +2098 | load y (98) into accumulator |
02 +3199 | sub x (99) from accumulator | |
03 +4200 | branch zero to unresolved location | |
25 rem increment y | none | rem ignored |
30 let y = y + 1 | 04 +2098 | load y into accumulator |
05 +3097 | add 1 (97) to accumulator | |
06 +2196 | store in temporary location 96 | |
07 +2096 | load from temporary location 96 | |
08 +2198 | store accumulator in y | |
35 rem add y to total | none | rem ignored |
40 let t = t + y | 09 +2095 | load t (95) into accumulator |
10 +3098 | add y to accumulator | |
11 +2194 | store in temporary location 94 | |
12 +2094 | load from temporary location 94 | |
13 +2195 | store accumulator in t | |
45 rem loop y | none | rem ignored |
50 goto 20 | 14 +4001 | branch to location 01 |
55 rem output result | none | rem ignored |
60 print t | 15 +1195 | output t to screen |
99 end | 16 +4300 | terminate execution |
Таблица символов
Символ | Тип | Адрес |
---|---|---|
5 | L | 00 |
10 | L | 00 |
'x' | V | 99 |
15 | L | 01 |
20 | L | 01 |
'y' | V | 98 |
25 | L | 04 |
30 | L | 04 |
1 | C | 97 |
35 | L | 09 |
40 | L | 09 |
't' | V | 95 |
45 | L | 14 |
50 | L | 14 |
55 | L | 15 |
60 | L | 15 |
99 | L | 16 |
При компиляции команды на строке 20 в таблице символов нет SML-адреса команды со строки 60, поэтому учитывая, что инструкция условного перехода расположена по адресу 03 в SML-памяти, мы помещаем номер Simple строки (60), куда должен быть сделан переход в ячейку 03 массива flags, для заполнения на втором проходе компилятора.
При компиляции надо следить за текущим адресом вывода команд, т.к. некоторые команды SML компилируются в несколько SML-инструкций.
При компиляции надо следить чтобы не случилось переполнение памяти Simpletron'а.
Первый проход компилятора
5 rem sum 1 to x
strtok отделяет номер строки программы, а функция atoi переводит символ в число 5.
Таблица символов дополняется элементом с типом L - номер строки и SML-адресом = 0.
Несмотря на то, что команда является комментарием запись в таблице символов позволит в дальнейшем использовать оператор goto или if...goto для перехода на эту строку.
Для комментариев не генерируется SML-инструкций и счётчик инструкций остаётся без изменений.
10 input x
Номер строки 10 также помещается в таблицу символов с типом L и адресом 0.
Команда input имеет в качестве параметра переменную x.
Компилятору необходимо найти адрес переменной в памяти SML. Он ищется в таблице символов и не находится, поэтому в таблицу добавляется запись ASCII-кода буквы x, с типом V и адресом 99 (место для переменных и констант выделяется с конца памяти Simpletron'а).
После этого генерируется инструкция SML. 10 - код ввода + 99 - адрес переменной формируют итоговую инструкцию.
Итоговая инструкция сохраняется в массиве SML по адресу 00 и счётчик инструкций увеличивается.
15 rem check y == x
Номер строки 15 помещается в таблицу символов с типом L и адресом 1.
20 if y == x goto 60
Номер строки 20 помещается в таблицу символов с типом L и адресом 1.
Команда if обозначает необходимость оценки условия.
Переменная y не существует в таблице символов, следовательно она добавляется: ASCII-код буквы y с типом V и адресом 98.
Далее создаются инструкции чтобы выполнить оценку условия. Для этого мы вычисляем разность y - x и используем инструкцию branch zero.
Загружаем y в аккумулятор (01 +2098). Затем вычитаем из аккумулятора x (02 +3199).
Затем добавляем инструкцию условного перехода (branch zero - 42). Из-за того, что SML-адрес строки 60 отсутствует в таблице символов на первом проходе инструкция будет неполной и адрес будет представлен нулём = 4200.
По индексу соответствующему SML-адресу неполной инструкции в массив flags мы помещаем номер строки программы Simple, SML-адрес которой надо будет добавить в инструкцию на втором проходе.
Увеличиваем счётчик инструкций до 4.
25 rem increment y
Номер строки 25 помещается в таблицу символов с типом L и адресом 4.
30 let y = y + 1
Номер строки 30 помещается в таблицу символов с типом L и адресом 4.
Команда let обозначает присваивание.
Изначально все символы выражения добавляются в таблицу символов, если их там ещё нет.
Константа (число 1) добавляется в таблицу символов с типом C и адресом 97.
Далее правая сторона команды let преобразуется из инфиксной в постфиксную нотацию (y + 1 => y 1 +) и оценивается с генерацией соответствующих инструкций.
При оценке мы помещаем в стек адреса переменной (y) и константы (1) вместо реальных значений.
Когда обрабатывается оператор сложения из стека извлекаются оба адреса и генерируется инструкции сложения.
04 +2098 (загружается y) 05 +3097 (добавляется 1)
Результат помещается во временную переменную по адресу 96 и этот адрес добавляется в стек.
06 +2196
Затем временная переменная загружается в аккумулятор и выгружается в переменную стоящую в левой части команды let, т.е. в нашем случае в переменную y.
07 +2096 (загружаем результат из временной переменной) 08 +2198 (сохраняем результат в y)
Достаточно очевидна избыточность последних двух инструкций.
35 rem add y to total
Номер строки 35 помещается в таблицу символов с типом L и адресом 9.
40 let t = t + y
Номер строки 40 помещается в таблицу символов с типом L и адресом 9. Команда похожа на команду строки 30.
Переменная t вставляется в таблицу символов с типом V и SML-адресом 95.
Инструкции следуют той же логике.
09 +2095 (загружаем t) t/95 10 +3098 (добавляем y) y/98 11 +2194 (сохраняем результат во временную переменную 94) 12 +2094 (загружаем результат обратно в аккумулятор) 13 +2195 (записываем результат в переменную t) t/95
45 rem loop y
Номер строки 45 помещается в таблицу символов с типом L и адресом 14.
50 goto 20
Номер строки 50 помещается в таблицу символов с типом L и адресом 14.
Аналогом команды безусловного перехода goto в SML-инструкциях является инструкция unconditional branch(40), которая передаёт управление инструкции с адресом полученным в качестве параметра.
В таблице символов существует элемент для строки 20 с SML-адресом равным 1.
Соответственно формируемая инструкция будет = 14 +4001
55 rem output result
Номер строки 55 помещается в таблицу символов с типом L и адресом 15.
60 print t
Номер строки 60 помещается в таблицу символов с типом L и адресом 15.
Команда вывода переводится в соответствующую инструкцию write(11)/
Адрес в SML-памяти переменной t находится в таблице символов = 95.
15 +1195
99 end
Конец программы соответствует инструкции остановки
16 +4300
Второй проход компилятора
В массиве flags мы ищем значения отличные от -1.
Элемент с индексом 3 содержит число 60. Это означает, что инструкция по SML-адресу 3 должна содержать в качестве оператора SML-адрес команды соответствующей строке 60 программы Simple.
Найдя в таблице символов элемент с типом L SML-адрес строки 60 равный 15 мы дополняем инструкцию 4200 адресом до состояния 4215.
Оптимизации компилятора
Оптимизация команды let
Попробуйте избавится при вычислении выражения командой let от двух избыточных инструкций и излишней временной переменной которые образуются при окончании вычислений.
Вместо
04 +2098 (load) 05 +3097 (add) 06 +2196 (store) 07 +2096 (load) 08 +2198 (store)
получить
04 +2098 (load) 05 +3097 (add) 06 +2198 (store)
Пары операций LOAD / STORE
Попробуйте избавится от ситуаций, когда данные выгружаются из аккумулятора и затем опять туда загружаются, т.е. убрать подобные пары инструкций.
12 +2196 (store) 13 +2096 (load)
Введение оператора вычисления остатка от деления
Введите в язык Simple уже доступный в SML оператор вычисления остатка от деления.
Введение оператора возведения в степень
Введите в язык Simple уже доступный в SML оператор возведения в степень.
Буквы верхнего регистра
Добавьте возможность распознания букв верхнего регистра в качестве имён переменных. 'A' как аналог 'a'.
Сделайте возможным ввод нескольких значений подряд
Модифицируйте команду input для ввода значений нескольких переменных.
Например: input x, y
Вывод нескольких значений
Модифицируйте команду print для вывода нескольких значений.
Например: print x, y
Ошибки компилятора
Дополните компилятор возможностями анализа ошибок в неправильно написанных командах Simple и вывода соответствующих ошибок компилятора.
Массивы целых чисел
Дополните язык Simple возможностью объявлять и использовать массивы целых чисел.
Подпрограммы
Дополните язык Simple возможностью вызывать подпрограммы командой gosub и возвращать поток выполнения назад командой return.
Цикл for
Дополните возможности языка Simple возможностями цикла вида:
for x = 2 to 10 step 2 rem Simple statements next или вида for x = 2 to 10 rem Simple statements next
с инкрементом по умолчанию равным +1
Строки
Дополните Simple и SML возможностями работы со строками.
Числа с плавающей точкой
Дополните Simple и SML возможностями работы с числами с плавающей точкой.
Базовый набор инструкций SML
Определите макросы препроцессора для определения инструкций.
Операции ввода/вывода: #define READ 10 - ввести число с клавиатуры в определённый адрес памяти #define WRITE 11 - вывести число из определённого адреса памяти на экран
Операции загрузки и выгрузки: #define LOAD 20 - загрузить число из определённого адреса памяти в аккумулятор #define STORE 21 - выгрузить число из аккумулятора в определённый адрес памяти
Арифметические операции: #define ADD 30 - добавить число из определённого адреса памяти в аккумулятор (результат остаётся в аккумуляторе) #define SUBTRACT 31 - отнять число из определённого адреса памяти из аккумулятора (результат остаётся в аккумуляторе) #define DIVIDE 32 - разделить число из определённого адреса памяти на значение аккумулятора (результат остаётся в аккумуляторе) #define MULTIPLY 33 - умножить число из определённого адреса памяти на значение аккумулятора (результат остаётся в аккумуляторе)
Операции передачи контроля: #define BRANCH 40 - Безусловный переход к исполнению инструкции по указанному адресу #define BRANCHNEG 41 - Переход к исполнению инструкции по указанному адресу в случае если аккумулятор содержит отрицательное число #define BRANCHZERO 42 - Переход к исполнению инструкции по указанному адресу в случае если аккумулятор содержит ноль #define HALT 43 - остановка компьютера