Устройство компилятора (кратко) на LLVM
Компилятор - инструмент конвертации исходного кода, написанного на высокоуровневом языке программирования в машинный код, который может исполнять компьютер.
Компилятор делится на 3 этапа:
FRONTEND - анализирует текст исходного кода и преобразует его в IR.
MIDDLE - анализирует и оптимизирует этот сгенерированный код IR.
BACKEND - преобразует IR в машинный код.
Сам компилятор, и собственно язык программирования состоит из нескольких частей.
Lexer - лексер
Лексер сканирует и превращает сырой текст в токены. То есть сам исходный код разбиваеты на набор токенов (такие как литералы, идентификаторы, ключевые слова, операторы, разделители)
Лексер читает исходный код символ за символом и идентифицирует последовательности символов, соответствующие определённым правилам языка.
Парсинг
Парсинг немного сложнее чем лексический анализ. Существует множество паресров и парсеров-генераторов.
Парсеры в компиляторах обычно принимают входные данные в форме токенов и строят определенное дерево - AST или дерево парсинга.
Обычно компиляторы строятся из множества маленьких компонентов, которые берут входные данные, меняют их или преобразуют их в различные выходные данные. Это одна из причин, по которым функциональные языки хорошо подходят для создания компиляторов. Другие причины — прекрасное сопоставление с эталоном и довольно обширные стандартные библиотеки. Прикольный факт: первая реализация компилятора Rust была на Ocaml.
Советую держать эти компоненты как можно более простыми и автономными — модульность сильно облегчит процесс. По-моему, то же можно сказать и о многих других аспектах разработки ПО.
AST
AST - абстрактное синтаксическое дерево. Это структурированное представление исходного кода программы в виде дерева, где каждый узел дерева представляет собой синтаксическую конструк языка программирования. Это дерево предоставляет абстракцию, которая позволяет анализировать и манипулировать программным кодом на высоком уровне.
IR
Эта часть занимается созданием [[1.2 IR]]. Через примитивы LLVM мы можем сгенерировать промежуточное представление. Каждому типу в AST дается метод, называемый codegen, который всегда возвращает объект значение LLVM, используемый для представления одного регистра присваивания (single assignment register), который является переменной для компилятора, которая может быть назначена только один раз. Интересно, что в этих примитивах IR то, что в отличии от ассемблера, они не зависят от какой-либо конкретной архитектуры машины, и это значительно упрощает работу для разработчиков языков, которым больше не нужно сопоставлять вывод в набор инструкций процессора. Теперь, когда фронтенд может генерировать IR, инструмент LLVM Optimizer используется для анализа и оптимизации сгенерированного кода. Он выполняет несколько проходов по IR и выполняет такие действия как устранение мертвого кода и скалярная замена агрегатов, и, наконец, это приводит нас к бекенду, где мы пишем модуль, который принимает IR в качестве входных данных, который выдает объектный код, который может работать на любой архитектуре.