Компилятор изнутри

Компилятор Оберона — четыре модуля, сто девять килобайт исходника. Его можно прочитать за выходные, и это, пожалуй, главная причина, по которой стоит брать именно эту систему: полный путь от текста до машинного кода здесь обозрим.

модуль размер что делает
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:

  1. Если индекс — константа, а длина массива известна, границы проверяются прямо при компиляции (bad index), а смещение просто прибавляется к адресу. Ни одной команды во время исполнения.

  2. Иначе индекс загружается в регистр и вставляется проверка границ:

Put1a(Cmp, RH, y.r, lim) ; сравнить индекс с длиной Trap(10, 1) ; если не меньше — ловушка номер 1

Cmp — это Sub с приёмником, который никуда не пишется: нужны только флаги. Trap(10, 1) даёт переход со связью через регистр MT по условию CC, с номером ошибки 1 в теле команды.

  1. Индекс умножается на размер элемента. Для четырёхбайтовых это сдвиг влево на два (Lsl), для остальных — настоящее умножение.

  2. Смещение складывается с базой, и получается адрес.

То есть каждое индексирование по переменному индексу стоит двух лишних команд: сравнение и условный переход. Сколько это на самом деле — в главе что мы измерили.

Для открытого массива длина неизвестна при компиляции, и её приходится читать из стека — там она лежит рядом с самим параметром.

Ловушки

Все проверки времени исполнения устроены одинаково: сравнение, затем условный переход через 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 поле перехода — ещё не смещение, а запись фиксапа.

Что стоит прочитать самому

Если браться за исходник, разумный порядок такой:

  1. ORS.Mod целиком — он маленький и задаёт словарь.
  2. В ORB.Mod — Import и Export: как устроен символьный файл.
  3. В ORG.Mod — Item, load, Index, Trap, Put0…Put3. Это ядро.
  4. В ORP.Mod — expression, StatSequence: увидите, как разбор и кодогенерация идут одним движением.

Четыре модуля, которые вы при этом читаете, — те самые, что собирают сами себя. Об этом — следующая глава.