Компилятор: что это такое
Compiler — программа, преобразующая исходный код в набор машинных кодов. Основная задача — создать исполняемый код, который будет запущен на целевой платформе (процессор или другое устройство). Один из самых известных обычным пользователям исполняемых файлов — .exe в Windows.
Compiler — «переводчик» с ЯП на язык машины. Процесс работы называют компиляцией или сборкой.
До появления компиляторов программисты писали код на машинном языке, который выглядел как набор чисел 101010101000 и так далее. Он был неудобным в плане читабельности, оптимизации, поддержки, но свою задачу решал. Правда, возникала проблема: каждый производитель процессора мог создавать собственные инструкции.
К примеру, чтобы запустить условную функцию суммирования на машине одного производителя, нужна команда 11111, а на процессоре другого — 01111. Эта разница делала практически невозможным запуск кода на разных устройствах. Чтобы программа работала на каждом из них, приходилось тратить время: найти участок кода, изменить, отладить, проверить. А если операций в исходном файле больше тысячи?
Так появились compilers — универсальные помощники, которые переводят язык программистов на машинный и помогают запускать проекты на разных устройствах.
Для чего нужен компилятор
В мире больше 30 ЯП. Одну и ту же задачу можно решить на Javascript, Python, php, Java, Go, Ruby и других. К примеру, нам нужно сложить два числа — 5 и 6, а затем вывести результат в консоль.
Javascript | Python | php | Ruby | Go |
function addNumbers() { return 5 + 6;}console.log(addNumbers()) | def add_numbers(): return 5 + 6print(add_numbers()) | function addNumbers() { return 5 + 6;}echo addNumbers() | def add_numbers 5 + 6endputs add_numbers | package mainimport "fmt"func addNumbers() int { return 5 + 6}func main() { sum := addNumbers() fmt.Println(sum)} |
Язык программирования и его синтаксис — своего рода абстракция, набор правил, которых должны придерживаться разработчики.
Машина не понимает синтаксис ЯП. Компьютер не умеет читать и распознавать let, var, console.log, print, Class, import, export, boolean, string и прочие. Для него эти слова как непонятный иностранный язык для человека.
Все, что умеет машина, — получать электрические сигналы и реагировать на них:
- 1 — сильный сигнал;
- 0 — слабый сигнал.
Так появился код, который понимает компьютер. К примеру, на языке машины число 1 выглядит как 00000001, число 2 — как 00000010. Это просто последовательность байт.
Compiler как раз и предназначен, чтобы:
- разобрать синтаксис написанного;
- проанализировать его;
- сгенерировать машинный код.
На входе программист отдает системе написанный код («исходники»), а на выходе получает исполняемый файл на языке, «понятном компьютеру». На небольших пет-проектах компиляция проходит за пару секунд: достаточно после написания кода дать команду npm run dev или npm run prod (на примере node.js и JavaScript). На крупных проектах компиляция может занимать несколько часов или даже суток.
Как работает компилятор
Мы уже знаем, что compiler получает исходник на определенном языке, а на выходе отдает код для машины.
Все переменные, циклы, массивы, классы и другие элементы синтаксиса превращаются в код вида:
00100101010101000001110010101010101001011
Компьютер умеет читать и распознавать эти инструкции.
Что происходит «под капотом»? Процесс компиляции состоит из нескольких условных этапов:
- исходный код;
- лексический анализ (токенизация, tokenizing);
- синтаксический анализ;
- семантический анализ;
- промежуточная генерация кода;
- оптимизация кода;
- генерация кода;
- машинный код.
Лексический анализ — этап, на котором compiler берет исходный код и разбивает на токены (лексемы). Это можно сравнить с тем, как мы разбиваем предложения на отдельные слова. На этом этапе компилятор не задумывается, работает ли код, какая логика в него заложена и т.д. Задача — найти и собрать слова/символы. Если упростить, то это function, if, else, return и так далее.
Токенизация кода функции
function addNumbers() {
return 5 + 6;
}
Синтаксический анализ — этап проверки: соответствует ли код формальным правилам синтаксиса языка: объявление переменных, вызов функции, пробелы, окончание строк, комментарии, ключевые и зарезервированные слова и так далее. На этой стадии compiler — строгий корректор, который ищет опечатки и неточности. Семантику языка и смысл проверяют на следующем этапе.
К примеру, мы ошиблись в слове return и не дописали одну букву: получилось retur. При компиляции не только показывается ошибка «Unexpected token», но и подсвечивается строка, чтобы разработчику было проще найти и устранить проблему.
Семантический анализ — проверка семантики кода. На прошлом этапе мы составили семантическое дерево. На этапе семантического анализа compiler разбирается, что можно делать с кодом. К примеру, если объявлена переменная, значит, с ней можно работать: присваивать значение, копировать значение и т.д. Компилятор работает с типами данных, областями видимости и другими правилами конкретного языка.
Допустим, программист объявил константу на JavaScript — неизменяемую переменную, которую нельзя перезаписать:
const GitVerse = 'Сайт gitverse.ru'
Попытаемся переопределить константу (изменить текст):
const GitVerse = 'Сайт gitverse.ru'
GitVerse = 'Хостинг Git-репозиториев'
В консоли появляется сообщение об ошибке «Assignment to constant variable». Разработчика предупреждают о том, что переопределить переменную нельзя.
Оптимизация кода — этап, на котором компилятор ищет варианты оптимизации: как уменьшить занимаемое место в памяти, ускорить работу. Самые простые примеры из жизни — не умножать на ноль, не открывать пустую коробку в поисках конфет.
К примеру, мы объявляем объект, Gitverse_list, но ничего в него не кладем. Строчка кода выглядит как Gitverse_list = []. Просим пройтись по нашему объекту циклом for, который должен вывести входящие в него элементы.
Сам код выглядит таким образом:
Gitverse_list = []
for i in Gitverse_list:
print(i)
else:
print ('Список пустой')
Результаты выполнения — выход на фразу 'Список пустой'.
Compiler понимает, что объект пустой, смысла заходить в него нет. Поэтому оптимизируется значительная часть «работы»: мы сразу выходим в блок else. Многие возможности оптимизации достигаются за счет того, что программа не выполняет код сразу.
Генерация кода — заключительный этап, на котором программа «выдает» инструкции для машины.
На каком языке написан компилятор
Компиляторы можно написать на любом языке программирования — высокоуровневом или низкоуровневом. Но традиционно одним из самых популярных считается ассемблер (assembly language. Это надстройка над машинным языком, которая не содержит объектов, списков и структур данных, а работает только с:
- мнемокодом (мнемоникой) — например, mov для пересылки из одного регистра в другой;
- операндами — регистрами, константными значениями;
- литерами — целыми числами, упакованными в машинный код;
- элементами выразительности — макросами, метками, комментариями.
Наша функция, которая складывает 5 и 6, на Ассемблере выглядит таким образом:
MOV BX, 5
MOV AX, 6
ADD AX, BX ; AX = 5 + 6 = 11
Почему у одного языка бывает несколько компиляторов
Как правило, первый Compiler для языка пишут создатели ЯП. Но в будущем сообщество разработчиков может предлагать собственные решения. Примеры — Java Compiler, Rust Compiler, Swift Compiler, Kotlin Compiler.
Если программист решил, что compiler плохо «переводит» написанный код в машинный, а доступ к коду программы есть, можно создать собственную реализацию. Каждая из них может быть разной. Все зависит от того, какую цель преследуют разработчики:
- обеспечить максимально быструю сборку проектов;
- минимизировать утечки памяти;
- собрать мусор и т. д.
Тем не менее, у всех компиляторов остается общая задача: перевод с языка программирования на язык машины.
Виды компиляторов
В зависимости от «направления перевода» выделяют следующие типы:
- Классические (ЯП —> машинный код) — переводят конкретный ЯП в машинный код. К примеру, g++ для C++.
- Обратные (машинный код —> ЯП) — возвращают скомпилированный машинный код в написанный на ЯП исходник. Тесно связаны с декомпиляторами и реверсингом.
- Кросс-компиляторы (один ЯП —> иной ЯП) — умеют работать с несколькими ЯП и «перегонять» код. Пример — GCC, который поддерживает C++, Objective-C, Java, Fortran и Go.
- Транспайлеры / транспиляторы (высокоуровневый ЯП —> код иного высокоуровневого ЯП) — умеют переводить код высокоуровневых ЯП. Пример подобного ПО — транспайлер Babel. Он умеет преобразовывать ECMAScript 2015+ в JavaScript.
К компиляторам как системам сборки относятся Makefile (UNIX- и Linux-системах) и cmake (в Windows-системах).
Под компиляцию с отдельных ЯП созданы:
- GNU C Compiler (GNU Compiler Collection) — изначально работал только с С, но затем добавились C++, Objective-C, Java, Фортран, Ada, Go;
- Clang — для Objective-C, C, C++, Objective-C++, OpenCL C;
- gfortran — для Фортрана или иных.
Компилятор, интерпретатор и транспайлер: отличия
Compiler — не единственный способ превратить исходный код в машинный. С развитием ЯП возникли и другие:
- интерпретаторы;
- JIT-компиляторы;
- транспиляторы.
Разберем их сходство и различия.
- Интерпретатор (interpretator, interpreter) работает по принципу синхронного переводчика. Он анализирует код и тут же выполняет его (покомандно или построчно). Программа пропускает такие этапы, как построение синтаксического дерева.
- JIT-компилятор (just-in-time, «точно в нужное время», динамическая компиляция, dynamic translation) компилирует байт-код в машинный код прямо во время работы.
- Транслятор — это преобразователь исходного кода одного ЯП на другой. Транспайлер (transpiler) — транслятор, преобразующий исходный код с тем же уровнем абстракции, который был у исходного ЯП.
Преимущества и недостатки компилируемых языков
В таблице ниже разберем основные особенности компилируемых и интерпретируемых ЯП, чтобы затем выделить преимущества и недостатки.
Критерий сравнения | Компилируемый ЯП | Интерпретируемый ЯП |
Как выглядит цепочка | Исходный код → компилятор → машинный код → вывод | Исходный код → интерпретатор → вывод |
Когда запускается программа | После компиляции, которая может занимать несколько часов или даже суток (зависит от проекта). После любых изменений в коде необходимо заново компилировать (собирать) проект | При исполнении |
Примеры ЯП | C, C++, Erlang, Haskell, Rust и Go | PHP, Perl, Ruby и Python |
Когда отображаются ошибки | Когда код выполнен | В процессе выполнения кода (построчно) |
Плюсы компилируемых ЯП:
- компиляция до исполнения, поэтому программы работают быстрее;
- оптимизация кода на этапе компиляции, поиск синтаксических ошибок;
- скрытие исходного кода.
Минусы:
- скорость компиляции может достигать нескольких часов или даже суток;
- необходимо пересобирать проект после любых изменений.
Как работать с компилятором
Программисты редко работают с compiler напрямую. Как правило, возможность компиляции и интерпретации представляется с платформой. В случае с NodeJS для сборки написанного в IDE кода достаточно использовать команды:
npm run dev
npm run prod
Те ошибки, которые «падают в консоль» («Unexpected token», «Cannot find module» и другие), — и есть результат работы Compiler.
Итак, мы разобрали, как происходит компиляция, какие инструменты используются, какие ошибки могут появляться. Для пет-проектов или командной работы стоит попробовать возможности российской платформы GitVerse для работы с исходным кодом.