Компилятор изнутри
Компилятор Оберона — четыре модуля, сто девять килобайт исходника. Его можно прочитать за выходные, и это, пожалуй, главная причина, по которой стоит брать именно эту систему: полный путь от текста до машинного кода здесь обозрим.
| модуль | размер | что делает |
|---|---|---|
ORS |
11 КБ | сканер: текст → лексемы |
ORB |
17 КБ | таблица имён и типов, чтение и запись символьных файлов |
ORG |
39 КБ | кодогенератор: элементы → команды RISC5 |
ORP |
42 КБ | синтаксический разбор; он же управляет всем остальным |
Схема без промежуточного представления: разбор порождает код напрямую.
ORP читает лексему, понимает, что это, и тут же зовёт ORG выдать команды.
Никакого дерева, никаких проходов оптимизации. Отсюда и скорость: вся система
пересобирается за минуты на машине в четыре мегагерца.
Элемент
Центральное понятие кодогенератора — элемент (Item). Это описание того,
где сейчас находится значение: в регистре, в памяти по смещению от базы, в
непосредственном поле команды, или это ещё не вычисленное условие перехода.
Item = RECORD
mode: INTEGER; (* где лежит: Const, Var, Par, Reg, RegI, Cond *)
type: ORB.Type;
a, b, r: LONGINT; (* смещение, база, регистр *)
rdo: BOOLEAN (* только для чтения *)
END
Кодогенерация — это переводы элемента из одного состояния в другое.
load(x) втаскивает значение в регистр, если оно ещё не там. Распределение
регистров — простой стек: RH («register high») указывает на первый
свободный, incR и DEC(RH) двигают его.
Как a[i] превращается в команды
Хороший пример, потому что виден весь стиль. Процедура Index в ORG.Mod:
-
Если индекс — константа, а длина массива известна, границы проверяются прямо при компиляции (
bad index), а смещение просто прибавляется к адресу. Ни одной команды во время исполнения. -
Иначе индекс загружается в регистр и вставляется проверка границ:
Put1a(Cmp, RH, y.r, lim) ; сравнить индекс с длиной
Trap(10, 1) ; если не меньше — ловушка номер 1
Cmp — это Sub с приёмником, который никуда не пишется: нужны только
флаги. Trap(10, 1) даёт переход со связью через регистр MT по условию
CC, с номером ошибки 1 в теле команды.
-
Индекс умножается на размер элемента. Для четырёхбайтовых это сдвиг влево на два (
Lsl), для остальных — настоящее умножение. -
Смещение складывается с базой, и получается адрес.
То есть каждое индексирование по переменному индексу стоит двух лишних команд: сравнение и условный переход. Сколько это на самом деле — в главе что мы измерили.
Для открытого массива длина неизвестна при компиляции, и её приходится читать из стека — там она лежит рядом с самим параметром.
Ловушки
Все проверки времени исполнения устроены одинаково: сравнение, затем условный
переход через MT со связью. В незанятые биты команды кодогенератор кладёт
позицию в исходном тексте и номер ошибки:
PROCEDURE Trap(cond, num: LONGINT);
BEGIN Put3(BLR, cond, ORS.Pos()*100H + num*10H + MT)
END Trap;
Железо эти биты игнорирует — для него это обычный переход по регистру. Достаёт их обработчик ловушки, прочитав команду, на которой всё сломалось. Приём экономный: ни таблицы адресов, ни отдельных структур — вся информация об ошибке уже в самой команде.
Номера, которые вы встретите чаще всего:
| номер | что значит |
|---|---|
| 1 | индекс вне границ массива |
| 2 | не прошла проверка типа |
| 3 | присваивание массива или записи несовместимого размера |
| 4 | разыменование NIL |
| 5 | вызов процедурной переменной, равной NIL |
| 6 | деление на ноль |
| 7 | не выполнилось ASSERT |
Фиксапы
Компилятор порождает код за один проход, а значит в момент вызова процедуры,
объявленной ниже, адрес ещё неизвестен. Решение классическое: в поле смещения
кладётся ссылка на предыдущее такое же место, и получается цепочка. Когда адрес
становится известен, FixLink проходит по цепочке и подставляет настоящее
значение.
То же самое делает загрузчик при вызовах в другие модули: в объектном файле
лежат цепочки, которые Modules доправляет при загрузке. Поэтому в .rsc
поле перехода — ещё не смещение, а запись фиксапа.
Что стоит прочитать самому
Если браться за исходник, разумный порядок такой:
ORS.Modцеликом — он маленький и задаёт словарь.- В
ORB.Mod—ImportиExport: как устроен символьный файл. - В
ORG.Mod—Item,load,Index,Trap,Put0…Put3. Это ядро. - В
ORP.Mod—expression,StatSequence: увидите, как разбор и кодогенерация идут одним движением.
Четыре модуля, которые вы при этом читаете, — те самые, что собирают сами себя. Об этом — следующая глава.