Введение

Компьютерная программа, которая переводит код с одного языка программирования на другой – программное обеспечение для перевода компьютерных языков. В информатике компилятор — это компьютерная программа, которая переводит компьютерный код, написанный на одном языке программирования (исходный язык), на другой язык (целевой язык). Термин "компилятор" в основном используется для программ, которые переводят исходный код с языка программирования высокого уровня на язык программирования низкого уровня (например, ассемблер, объектный код или машинный код) для создания исполняемой программы. Существует множество различных типов компиляторов, которые генерируют вывод в различных полезных форматах. Кросс-компилятор генерирует код для другого процессора или операционной системы, чем та, на которой работает сам кросс-компилятор. Bootstrap-компилятор часто является временным компилятором, используемым для компиляции более постоянного или лучше оптимизированного компилятора для языка. Связанное программное обеспечение включает декомпиляторы, программы, которые переводят с языков низкого уровня на языки более высокого уровня; программы, которые переводят между языками высокого уровня, обычно называемые компиляторами или транспиляторами из источника в источник; переписыватели языка, обычно программы, которые переводят форму выражений без изменения языка; и компиляторы компиляторов, компиляторы, которые генерируют компиляторы (или их части), часто в общем и повторно используемом виде, чтобы иметь возможность генерировать множество различных компиляторов. Компилятор, вероятно, выполняет некоторые или все из следующих операций, часто называемых фазами: предварительная обработка, лексический анализ, парсинг, семантический анализ (синтаксически-ориентированный перевод), преобразование входных программ в промежуточное представление, оптимизация кода и генерация машинного кода. Компиляторы обычно реализуют эти фазы как модульные компоненты, способствуя эффективному проектированию и корректности преобразований исходного ввода в целевой вывод. Ошибки программы, вызванные некорректным поведением компилятора, могут быть очень сложными для отладки и обхода; поэтому разработчики компиляторов прилагают значительные усилия для обеспечения корректности компилятора. Компиляторы — не единственный языковой процессор, используемый для преобразования исходных программ. Интерпретатор — это компьютерное программное обеспечение, которое преобразует и затем выполняет указанные операции. Форма Бэкуса — Наура (BNF) описывает синтаксис "предложений" языка. Она была разработана Джоном Бэкусом и использовалась для синтаксиса Algol 60. Идеи происходят из концепций контекстно-свободной грамматики лингвиста Ноама Хомского. "BNF и его расширения стали стандартными инструментами для описания синтаксиса языков программирования. Во многих случаях части компиляторов генерируются автоматически из описания BNF". Между 1942 и 1945 годами Конрад Цузе разработал первый (алгоритмический) язык программирования для компьютеров под названием Plankalkül ("плановый исчисление"). Цузе также представил Planfertigungsgerät ("устройство сборки планов") для автоматического перевода математической формулировки программы в машиночитаемую перфорированную плёнку. APL — это язык для математических вычислений. В период с 1949 по 1951 год Хайнц Рутисхаузер предложил Superplan, язык высокого уровня и автоматический переводчик. COBOL (Common Business Oriented Language) развился из A0 и FLOW-MATIC, чтобы стать доминирующим языком высокого уровня для бизнес-приложений. LISP (List Processor) для символьных вычислений. Технология компиляции развилась из необходимости строго определенного преобразования исходной программы высокого уровня в целевую программу низкого уровня для цифрового компьютера. Компилятор можно рассматривать как front-end для анализа исходного кода и back-end для синтеза анализа в целевой код. Оптимизация между front-end и back-end может создать более эффективный целевой код. Некоторые ранние вехи в развитии технологии компиляции: май 1952 года: команда Грейс Хоппер в Remington Rand написала компилятор для языка программирования A0 (и ввела термин "компилятор" для его описания), хотя компилятор A0 функционировал скорее как загрузчик или линковщик, чем как современное представление полного компилятора. 1952 год, до сентября: Autocode-компилятор, разработанный Аликом Гленни для компьютера Manchester Mark I в Манчестерском университете, некоторыми считается первым компилируемым языком программирования. 1954–1957: команда под руководством Джона Бэкуса в IBM разработала FORTRAN, который обычно считается первым языком программирования высокого уровня. В 1957 году они завершили FORTRAN-компилятор, который обычно считается первым однозначно полным компилятором. 1959 год: Конференция по языкам систем данных (CODASYL) инициировала разработку COBOL. Дизайн COBOL опирался на A0 и FLOW-MATIC. К началу 1960-х годов COBOL компилировался на множестве архитектур. 1958–1960: Algol 58 был предшественником ALGOL 60. Он представил блоки кода, важный шаг в развитии структурированного программирования. ALGOL 60 был первым языком, реализовавшим вложенные определения функций с лексической областью видимости. Он включал рекурсию. Его синтаксис был определен с использованием BNF. ALGOL 60 вдохновил многие языки, которые последовали за ним. Тони Хоар заметил: "это было не только улучшением по сравнению с его предшественниками, но и почти со всеми его последователями". 1958–1962: Джон Маккарти из MIT разработал LISP. Возможности обработки символов обеспечили полезные функции для исследований в области искусственного интеллекта. В 1962 году в выпуске LISP 1.5 были отмечены некоторые инструменты: интерпретатор, написанный Стивеном Расселом и Даниэлем Дж. Эдвардсом, компилятор и ассемблер, написанные Тимом Хартом и Майком Левином. Ранние операционные системы и программное обеспечение были написаны на ассемблере. В 1960-х и начале 1970-х годов использование языков высокого уровня для системного программирования все еще было спорным из-за ограничений ресурсов. Однако несколько исследовательских и промышленных усилий начали переход к языкам системного программирования высокого уровня, например, BCPL, BLISS, B и C.

BCPL (Basic Combined Programming Language), разработанный в 1966 году Мартином Ричардсом в Кембриджском университете, изначально был разработан как инструмент для написания компиляторов. Было реализовано несколько компиляторов, книга Ричардса дает представление о языке и его компиляторе. BCPL был не только влиятельным языком системного программирования, который до сих пор используется в исследованиях, но и послужил основой для разработки языков B и C. BLISS (Basic Language for Implementation of System Software) был разработан для компьютера Digital Equipment Corporation (DEC) PDP 10 исследовательской группой В. А. Вульфа из Carnegie Mellon University (CMU). Команда CMU разработала компилятор BLISS 11 годом позже, в 1970 году. Multics (Multiplexed Information and Computing Service), проект операционной системы с разделением времени, включал MIT, Bell Labs, General Electric (позже Honeywell) и возглавлялся Фернандо Корбато из MIT. Multics был написан на языке PL/I, разработанном IBM и IBM User Group. Целью IBM было удовлетворить требования бизнес-, научных и системных программ. Существовали и другие языки, которые можно было бы рассмотреть, но PL/I предлагал наиболее полное решение, хотя он еще не был реализован. В первые несколько лет проекта Multics подмножество языка можно было компилировать в ассемблерный код с помощью Early PL/I (EPL) компилятора Дуга МакИлори и Боба Морриса из Bell Labs. EPL поддерживал проект до тех пор, пока не был разработан bootstrap-компилятор для полного PL/I. Bell Labs покинула проект Multics в 1969 году и разработала язык системного программирования B, основанный на концепциях BCPL, написанный Деннисом Ритчи и Кеном Томпсоном. Ритчи создал bootstrap-компилятор для B и написал операционную систему Unics (Uniplexed Information and Computing Service) для PDP 7 на B. Unics в конечном итоге стал называться Unix. Bell Labs начала разработку и расширение C на основе B и BCPL. Компилятор BCPL был перенесен в Multics компанией Bell Labs, и BCPL был предпочтительным языком в Bell Labs. Первоначально использовалась front-end программа для B-компилятора Bell Labs, пока разрабатывался C-компилятор. В 1971 году новый PDP 11 предоставил рес...

Конструкция компилятора

Компилятор реализует формальное преобразование из программы на языке высокого уровня в программу на языке низкого уровня. Проектирование компилятора может представлять собой комплексное решение или сосредоточиться на определенной подзадаче, взаимодействующей с другими инструментами компиляции, например, с препроцессорами, ассемблерами и компоновщиками. Требования к проектированию включают в себя строго определенные интерфейсы как внутри самого компилятора между его компонентами, так и снаружи – между вспомогательными инструментами. На заре развития компиляторов подход к их проектированию напрямую зависел от сложности обрабатываемого языка программирования, опыта разработчика (или разработчиков) и доступных ресурсов. Ограниченность ресурсов обусловила необходимость многократного прохода по исходному коду. Компилятор для относительно простого языка, разработанный одним человеком, мог представлять собой единое монолитное программное обеспечение. Однако с ростом сложности исходного языка проектирование может быть разделено на несколько взаимосвязанных этапов. Разделение на этапы позволяет улучшить структуру компилятора и сосредоточить разработку на отдельных функциях процесса компиляции.

Однопроходные компиляторы против многопроходных

Классификация компиляторов по количеству проходов обусловлена ограничениями аппаратных ресурсов компьютеров. Компиляция требует значительных вычислительных затрат, и ранние компьютеры не имели достаточного объема памяти для размещения одной программы, выполняющей весь этот объем работы. В результате компиляторы были разделены на более мелкие программы, каждая из которых выполняла проход по исходному коду (или его представлению), осуществляя часть необходимого анализа и преобразований. Возможность компиляции за один проход традиционно считалась преимуществом, поскольку она упрощает разработку компилятора, а компиляторы с одним проходом обычно выполняют компиляцию быстрее, чем многопроходные компиляторы. Таким образом, отчасти из-за ограниченности ресурсов ранних систем, многие ранние языки программирования были специально разработаны для компиляции за один проход (например, Паскаль). В некоторых случаях, конструкция определенной языковой конструкции может потребовать от компилятора выполнения более одного прохода по исходному коду. Например, рассмотрим объявление, расположенное в строке 20 исходного кода, которое влияет на преобразование оператора, расположенного в строке 10. В этом случае первый проход должен собрать информацию об объявлениях, следующих за операторами, на которые они влияют, а фактическое преобразование происходит на последующем проходе. Недостатком компиляции за один проход является невозможность выполнения многих сложных оптимизаций, необходимых для генерации высококачественного кода. Точно определить количество проходов, выполняемых оптимизирующим компилятором, может быть затруднительно. Например, различные этапы оптимизации могут многократно анализировать одно выражение, но анализировать другое выражение только один раз. Разделение компилятора на небольшие программы – это метод, используемый исследователями, заинтересованными в создании компиляторов, корректность которых можно доказать. Доказательство корректности набора небольших программ часто требует меньше усилий, чем доказательство корректности одной большой эквивалентной программы.

Трехступенчатая структура компилятора

Независимо от точного количества фаз в разработке компилятора, эти фазы можно отнести к одной из трех стадий: фронтенду, миддленду и бэкенду. Фронтенд сканирует входные данные и проверяет синтаксис и семантику в соответствии с конкретным исходным языком. Для языков со статической типизацией он выполняет проверку типов, собирая информацию о типах. Если входная программа содержит синтаксические ошибки или ошибки типов, он генерирует сообщения об ошибках и/или предупреждения, обычно указывая местоположение в исходном коде, где проблема была обнаружена; в некоторых случаях фактическая ошибка может находиться (значительно) раньше в программе. К аспектам фронтенда относятся лексический, синтаксический и семантический анализ. Фронтенд преобразует входную программу в промежуточное представление (IR) для дальнейшей обработки миддлендом. Это IR обычно представляет собой представление программы более низкого уровня по сравнению с исходным кодом. Миддленд выполняет оптимизации IR, не зависящие от целевой архитектуры процессора. Эта независимость от исходного кода и машинного кода позволяет совместно использовать общие оптимизации между версиями компилятора, поддерживающими разные языки и целевые процессоры. Примерами оптимизаций миддленда являются удаление бесполезного кода (устранение мертвого кода) или недостижимого кода (анализ достижимости), обнаружение и распространение констант (распространение констант), перемещение вычислений в менее часто выполняемое место (например, за пределы цикла) или специализация вычислений на основе контекста, в конечном итоге создавая "оптимизированный" IR, который используется бэкендом. Бэкенд принимает оптимизированный IR от миддленда. Он может выполнять дополнительный анализ, преобразования и оптимизации, специфичные для целевой архитектуры процессора. Бэкенд генерирует целевой машинный код, выполняя распределение регистров в процессе. Бэкенд выполняет планирование инструкций, переупорядочивая их для поддержания занятости параллельных исполнительных устройств за счет заполнения интервалов задержки. Хотя большинство задач оптимизации являются NP-трудными, эвристические методы их решения хорошо разработаны и реализованы в компиляторах промышленного качества. Обычно выходные данные бэкенда – это машинный код, специализированный для конкретного процессора и операционной системы. Такой подход с использованием фронтенда, миддленда и бэкенда позволяет объединять фронтенды для разных языков с бэкендами для разных процессоров, при этом совместно используя оптимизации миддленда. Практическими примерами этого подхода являются GNU Compiler Collection, Clang (компилятор C/C++ на основе LLVM) и Amsterdam Compiler Kit, которые имеют несколько фронтендов, общие оптимизации и несколько бэкендов.

Задняя часть

Задний конец отвечает за оптимизации, специфичные для архитектуры процессора, и за генерацию кода. Ярким примером является оптимизация "подсматриванием", которая переписывает короткие последовательности инструкций ассемблера в более эффективные инструкции. Генерация кода: преобразованный промежуточный язык транслируется в целевой язык, обычно в машинный язык системы. Это включает в себя принятие решений об использовании ресурсов и памяти, таких как определение того, какие переменные поместить в регистры, а какие – в память, а также выбор и планирование соответствующих машинных инструкций с использованием подходящих режимов адресации (см. также алгоритм Сети-Уллмана). Для упрощения отладки также может потребоваться генерация данных отладки.

Правильность компилятора

Корректность компилятора — это область программной инженерии, занимающаяся доказательством соответствия поведения компилятора спецификации языка. Методы включают разработку компилятора с применением формальных методов и проведение тщательного тестирования (часто называемого валидацией компилятора) для уже существующего компилятора.

Сборник и интерпретированный языки

Языки программирования высокого уровня обычно создаются с учетом определенного типа трансляции: либо как компилируемые, либо как интерпретируемые. Однако на практике редко можно найти что-то в языке, что однозначно требовало бы исключительно компиляции или исключительно интерпретации, хотя можно спроектировать языки, полагающиеся на повторную интерпретацию во время выполнения. Категоризация обычно отражает наиболее популярные или распространенные реализации языка – например, BASIC часто называют интерпретируемым языком, а C – компилируемым, несмотря на существование компиляторов BASIC и интерпретаторов C. Интерпретация не отменяет компиляцию полностью, она лишь скрывает ее от пользователя и делает поэтапной. Даже если сам интерпретатор может быть интерпретирован, где-то внизу стека выполнения все равно необходим набор непосредственно исполняемых машинных инструкций (см. машинный язык). Более того, для оптимизации компиляторы могут включать функциональность интерпретатора, а интерпретаторы – методы компиляции во время выполнения. Например, если выражение можно вычислить во время компиляции и вставить результат в выходную программу, то это предотвращает его пересчет при каждом запуске программы, что может значительно ускорить работу. Современные тенденции к компиляции "точно в срок" (just-in-time) и интерпретации байт-кода еще больше размывают традиционные границы между компиляторами и интерпретаторами. Некоторые спецификации языков предписывают, чтобы реализации включали средства компиляции, например, Common Lisp. Однако в определении Common Lisp нет ничего, что препятствовало бы его интерпретации. Другие языки имеют функции, которые легко реализовать в интерпретаторе, но значительно усложняют написание компилятора; например, APL, SNOBOL4 и многие скриптовые языки позволяют программам создавать произвольный исходный код во время выполнения с помощью стандартных строковых операций, а затем выполнять этот код, передавая его специальной функции вычисления. Для реализации этих функций в компилируемом языке программы обычно должны поставляться с библиотекой времени выполнения, включающей версию самого компилятора.