Простой генератор кода
Дизайн компилятора — важный компонент конструкции компилятора. Он включает в себя множество различных задач, таких как анализ исходного кода и создание из него промежуточного представления (IR), оптимизация IR для создания целевого машинного кода и создание внешних представлений (OR) для программ, используемых при отладке или тестировании. В этой статье мы описываем наши усилия по улучшению конструкции генераторов простых языков. Мы представляем новый многоразовый компонент под названием «Генератор простого кода» (SCG), который реализует несколько функций, упрощающих создание простых генераторов кода для любого языка программирования. Компонент SCG состоит из двух частей: во-первых, он содержит синтаксический анализатор, который преобразует текстовые входные данные в абстрактное синтаксическое дерево; во-вторых, его сгенерированный AST имеет выражения в символической форме, где это возможно, вместо простого представления их в виде строк, как это делает большинство других компиляторов сегодня.
Генератор кода — это компилятор, который транслирует промежуточное представление исходной программы в целевую программу. Другими словами, генератор кода переводит абстрактное синтаксическое дерево в машинно-зависимый исполняемый код. Процесс создания машинно-зависимого вывода из абстрактного синтаксического дерева включает два шага: один для построения абстрактного синтаксического дерева, а другой для генерации соответствующего ему машинного кода.
Первый шаг включает в себя построение абстрактного синтаксического дерева (AST) путем обхода всех возможных путей во входных файлах. Это дерево будет содержать информацию о каждом бите данных в вашей программе по мере того, как они встречаются во время синтаксического анализа или во время выполнения; важно отметить, что это может происходить как во время компиляции (как часть компиляции), так и во время выполнения (в некоторых случаях).
Зарегистрировать дескриптор
Дескрипторы регистров — это структуры данных, в которых хранится информация о регистрах, используемых в программе. Это включает в себя регистрационный номер и его имя, а также его тип. Компилятор использует эту информацию при генерации машинного кода для вашей программы, поэтому важно поддерживать ее в актуальном состоянии при написании кода!
Компилятор использует регистровый файл, чтобы определить, какие значения будут доступны для использования в вашей программе. Это делается путем обхода каждого из регистров и определения, содержат ли они достоверные данные или нет. Если в реестре ничего нет, то его можно использовать для других целей!
Дескриптор адреса
Дескриптор адреса используется для представления областей памяти, используемых программой. Дескрипторы адресов создаются функцией getReg , которая возвращает структуру, содержащую информацию о том, как получить доступ к памяти. Дескрипторы адресов могут быть созданы для любой инструкции в коде вашей программы и сохранены в регистрах или в стеке; однако в любой момент времени будет существовать только один экземпляр дескриптора адреса (если не выполняется другой поток).
Когда пользователь хочет получить данные из произвольного места в исходном коде программы с помощью getReg, вызовите этот метод с двумя аргументами: первый аргумент указывает, какой регистр содержит желаемое значение (например, «M»), а второй аргумент указывает, где точно в этом регистре он должен быть помещен обратно в исходное место хранения на диске/в памяти, прежде чем снова вернуться в основную память после успешного доступа к его содержимому с помощью косвенных вызовов, таких как LoadFromBuffer() или StoreToBuffer().
Алгоритм генерации кода
Алгоритм генерации кода является ядром компилятора. Он устанавливает дескрипторы регистров и адресов, а затем генерирует машинные инструкции, которые дают вам контроль над вашей программой на уровне процессора.
Алгоритм разделен на четыре части: установка дескриптора регистра, генерация базового блока, генерация инструкций для операций с регистрами (например, сложение) и завершение базового блока оператором перехода или командой возврата.
Настройка дескриптора регистра: эта часть устанавливает значение отдельного регистра в пространстве памяти, помещая его индекс в массив всех возможных значений для этого типа регистра (i32). Он также хранит информацию о том, какая операция была выполнена с ним, чтобы последующие шаги могли определить, какая операция произошла, если они вызываются несколько раз во время выполнения.
Генерация базового блока: этот шаг включает в себя создание отдельных блоков внутри каждого базового блока, а также линий между ними, чтобы мы могли отслеживать, где что происходит в любой момент во время выполнения.
Генерация инструкций для операций с регистрами: на этом этапе операторы исходного кода преобразуются в машинные инструкции, используя информацию как из наших файлов формата файлов ELF (сгенерированных GCC), так и из других источников, таких как система сборки Bazel, которая знает, как генерировать определенные виды команд. машинный код для конкретных процессоров. Именно здесь мы начинаем видеть волшебство того, как компиляторы работают на практике, поскольку они способны генерировать код, оптимизированный различными способами в зависимости от типа выполняемой операции (например, сложения) и задействованных регистров (i32). Этот шаг также можно рассматривать как «распределение регистров», потому что именно здесь мы определяем, какие регистры будут использоваться для каждой операции и сколько их всего. На этом шаге используется информация, сгенерированная на предыдущих шагах, а также другая информация, такая как правила о том, сколько регистров необходимо для определенных операций. Например, мы можем знать, что для 32-битного сложения требуется два регистра: один для хранения добавляемого значения, а другой — для результата этой операции.
Планирование инструкций: на этом шаге инструкции переупорядочиваются таким образом, чтобы они эффективно выполнялись на конкретной архитектуре ЦП. На этом шаге используется информация о ресурсах выполнения, доступных в каждой архитектуре ЦП, чтобы определить наилучший порядок выполнения операций. Он также учитывает такие вещи, как достаточно ли у нас регистров для хранения значений (если некоторые из них используются) или есть ли узкое место где-то еще в конвейере.
Дизайн функции getReg
Функция getReg — это основная функция, которая возвращает значение переданного регистра. Она использует два параметра: номер регистра и действие, которое над ним нужно выполнить. Когда вы вызываете getReg без параметров, он возвращает значения всех регистров (т. е. всех регистров).
Если вы хотите вернуть определенное значение регистра, вы можете вызвать getReg с этим номером регистра и ничем другим; если есть другие параметры после этого (например, 2-й параметр), то они будут искать связанные с типом этого первого параметра, а не добавляться в качестве еще одного аргумента после того, как все остальное уже было оценено - таким образом, мы не тратьте время на обработку данных, когда вообще ничего не происходит! Если после этих двух типов ничего нет, кроме пустой строки (" "); тогда тоже ничего не происходит!
Результатом этой фазы является последовательность машинных инструкций, которые могут быть выполнены с помощью системы времени выполнения. Этот генератор кода генерирует язык ассемблера для целевого компьютера и объектный код для целевого компьютера. Генератор кода отвечает за создание языка ассемблера для целевого компьютера. Он принимает на вход промежуточный формат (иногда называемый компилятором IR), который был обработан синтаксическим анализатором и средством проверки типов, но еще не опущен в машинный код.
Генератор кода также отвечает за создание объектного кода, который может быть выполнен на целевом компьютере. Этот объектный код обычно имеет формат, специфичный для целевой архитектуры, такой как Intel 8086 или Motorola 68000.
Внешний интерфейс компилятора анализирует исходный код и выполняет его начальный анализ. Затем он пропускает эти данные через несколько этапов компиляции, превращая их в машинные инструкции, которые могут выполняться на процессоре компьютера.
Вывод
Создание генераторов кода может быть очень сложной задачей. Вывод такого генератора кода должен быть максимально читабельным и лаконичным, без посторонних шумов и помех.