Включите исполнение JavaScript в браузере, чтобы запустить приложение.
11 фев 2025

Компилятор: что это такое и для чего он нужен в программировании

В статье разбираем, что такое компилятор (Compiler), в чем его отличие от интерпретатора, как он работает и в чем его назначение.

Компилятор: что это такое

Compiler — программа, преобразующая исходный код в набор машинных кодов. Основная задача — создать исполняемый код, который будет запущен на целевой платформе (процессор или другое устройство). Один из самых известных обычным пользователям исполняемых файлов — .exe в Windows.

Compiler — «переводчик» с ЯП на язык машины. Процесс работы называют компиляцией или сборкой. 

До появления компиляторов программисты писали код на машинном языке, который выглядел как набор чисел 101010101000 и так далее. Он был неудобным в плане читабельности, оптимизации, поддержки, но свою задачу решал. Правда, возникала проблема: каждый производитель процессора мог создавать собственные инструкции. 

К примеру, чтобы запустить условную функцию суммирования на машине одного производителя, нужна команда 11111, а на процессоре другого — 01111. Эта разница делала практически невозможным запуск кода на разных устройствах. Чтобы программа работала на каждом из них, приходилось тратить время: найти участок кода, изменить, отладить, проверить. А если операций в исходном файле больше тысячи? 

Так появились compilers — универсальные помощники, которые переводят язык программистов на машинный и помогают запускать проекты на разных устройствах.

Для чего нужен компилятор

В мире больше 30 ЯП. Одну и ту же задачу можно решить на Javascript, Python, php, Java, Go, Ruby и других. К примеру, нам нужно сложить два числа — 5 и 6, а затем вывести результат в консоль. 

Javascript PythonphpRubyGo
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_numberspackage 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-репозиториев'
javascript

В консоли появляется сообщение об ошибке «Assignment to constant variable». Разработчика предупреждают о том, что переопределить переменную нельзя.

Оптимизация кода — этап, на котором компилятор ищет варианты оптимизации: как уменьшить занимаемое место в памяти, ускорить работу. Самые простые примеры из жизни — не умножать на ноль, не открывать пустую коробку в поисках конфет.

К примеру, мы объявляем объект, Gitverse_list, но ничего в него не кладем. Строчка кода выглядит как Gitverse_list = []. Просим пройтись по нашему объекту циклом for, который должен вывести входящие в него элементы.

Сам код выглядит таким образом:

Gitverse_list = [] 

for i in Gitverse_list: 

 print(i) 

else: 

 print ('Список пустой')
javascript

Результаты выполнения — выход на фразу 'Список пустой'.

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 плохо «переводит» написанный код в машинный, а доступ к коду программы есть, можно создать собственную реализацию. Каждая из них может быть разной. Все зависит от того, какую цель преследуют разработчики: 

  • обеспечить максимально быструю сборку проектов;
  • минимизировать утечки памяти;
  • собрать мусор и т. д. 

Тем не менее, у всех компиляторов остается общая задача: перевод с языка программирования на язык машины.

Виды компиляторов

В зависимости от «направления перевода» выделяют следующие типы:

  1. Классические (ЯП —> машинный код) — переводят конкретный ЯП в машинный код. К примеру, g++ для C++.
  2. Обратные (машинный код —> ЯП) — возвращают скомпилированный машинный код в написанный на ЯП исходник. Тесно связаны с декомпиляторами и реверсингом.
  3. Кросс-компиляторы (один ЯП —> иной ЯП) — умеют работать с несколькими ЯП и «перегонять» код. Пример — GCC, который поддерживает C++, Objective-C, Java, Fortran и Go.
  4. Транспайлеры / транспиляторы (высокоуровневый ЯП —> код иного высокоуровневого ЯП) — умеют переводить код высокоуровневых ЯП. Пример подобного ПО — транспайлер 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-компиляторы;
  • транспиляторы.

Разберем их сходство и различия.

  1. Интерпретатор (interpretator, interpreter) работает по принципу синхронного переводчика. Он анализирует код и тут же выполняет его (покомандно или построчно). Программа пропускает такие этапы, как построение синтаксического дерева.
  2. JIT-компилятор (just-in-time, «точно в нужное время», динамическая компиляция, dynamic translation) компилирует байт-код в машинный код прямо во время работы. 
  3. Транслятор — это преобразователь исходного кода одного ЯП на другой. Транспайлер (transpiler) — транслятор, преобразующий исходный код с тем же уровнем абстракции, который был у исходного ЯП.

Преимущества и недостатки компилируемых языков

В таблице ниже разберем основные особенности компилируемых и интерпретируемых ЯП, чтобы затем выделить преимущества и недостатки.

Критерий сравненияКомпилируемый ЯПИнтерпретируемый ЯП
Как выглядит цепочкаИсходный код → компилятор → машинный код → выводИсходный код → интерпретатор → вывод
Когда запускается программаПосле компиляции, которая может занимать несколько часов или даже суток (зависит от проекта). После любых изменений в коде необходимо заново компилировать (собирать) проект При исполнении
Примеры ЯПC, C++, Erlang, Haskell, Rust и GoPHP, Perl, Ruby и Python
Когда отображаются ошибкиКогда код выполненВ процессе выполнения кода (построчно)

Плюсы компилируемых ЯП:

  • компиляция до исполнения, поэтому программы работают быстрее;
  • оптимизация кода на этапе компиляции, поиск синтаксических ошибок;
  • скрытие исходного кода.

Минусы:

  • скорость компиляции может достигать нескольких часов или даже суток;
  • необходимо пересобирать проект после любых изменений.

Как работать с компилятором

Программисты редко работают с compiler напрямую. Как правило, возможность компиляции и интерпретации представляется с платформой. В случае с NodeJS для сборки написанного в IDE кода достаточно использовать команды:

npm run dev

npm run prod

Те ошибки, которые «падают в консоль» («Unexpected token», «Cannot find module» и другие), — и есть результат работы Compiler.

Итак, мы разобрали, как происходит компиляция, какие инструменты используются, какие ошибки могут появляться. Для пет-проектов или командной работы стоит попробовать возможности российской платформы GitVerse для работы с исходным кодом.