В вычислительной технике компилятор — это компьютерная программа , которая переводит компьютерный код, написанный на одном языке программирования ( исходный язык), на другой язык ( целевой язык). Название «компилятор» в основном используется для программ, которые переводят исходный код с языка программирования высокого уровня на язык программирования низкого уровня (например, язык ассемблера , объектный код или машинный код ) для создания исполняемой программы. [1] [2] : p1 [3]
Существует множество различных типов компиляторов, которые производят вывод в различных полезных формах. Кросс-компилятор производит код для другого процессора или операционной системы, чем тот, на котором работает сам кросс-компилятор. Компилятор начальной загрузки часто является временным компилятором, используемым для компиляции более постоянного или лучше оптимизированного компилятора для языка.
Сопутствующее программное обеспечение включает декомпиляторы — программы, которые переводят с языков низкого уровня на языки более высокого уровня; программы, которые переводят между языками высокого уровня, обычно называемые компиляторами исходного кода или транспиляторами ; рерайтеры языков — обычно программы, которые переводят форму выражений без изменения языка; и компиляторы-компиляторы — компиляторы, которые производят компиляторы (или их части), часто универсальным и повторно используемым способом, чтобы иметь возможность производить множество различных компиляторов.
Компилятор, скорее всего, выполнит некоторые или все из следующих операций, часто называемых фазами: предварительная обработка , лексический анализ , синтаксический анализ , семантический анализ ( синтаксически-управляемый перевод ), преобразование входных программ в промежуточное представление , оптимизация кода и машинно-специфическая генерация кода . Компиляторы обычно реализуют эти фазы как модульные компоненты, способствуя эффективному проектированию и корректности преобразований исходного ввода в целевой вывод. Ошибки программы, вызванные неправильным поведением компилятора, может быть очень трудно отследить и обойти; поэтому разработчики компиляторов вкладывают значительные усилия, чтобы обеспечить корректность компилятора . [4]
Компиляторы — не единственный языковой процессор, используемый для преобразования исходных программ. Интерпретатор — это компьютерное программное обеспечение, которое преобразует и затем выполняет указанные операции. [2] : p2 Процесс трансляции влияет на разработку компьютерных языков, что приводит к предпочтению компиляции или интерпретации. Теоретически язык программирования может иметь как компилятор, так и интерпретатор. На практике языки программирования, как правило, связаны только с одним (компилятором или интерпретатором).
Теоретические вычислительные концепции, разработанные учеными, математиками и инженерами, легли в основу развития современных цифровых вычислений во время Второй мировой войны. Примитивные двоичные языки развивались, поскольку цифровые устройства понимали только единицы и нули и схемы схем в базовой архитектуре машины. В конце 1940-х годов были созданы языки ассемблера, чтобы предложить более работоспособную абстракцию компьютерных архитектур. [5] Ограниченный объем памяти ранних компьютеров привел к существенным техническим проблемам при проектировании первых компиляторов. Поэтому процесс компиляции необходимо было разделить на несколько небольших программ. Программы front-end производят продукты анализа, используемые программами back-end для генерации целевого кода. Поскольку компьютерные технологии предоставляли больше ресурсов, конструкции компиляторов могли лучше соответствовать процессу компиляции.
Обычно программисту более продуктивно использовать язык высокого уровня, поэтому разработка языков высокого уровня естественным образом вытекала из возможностей, предлагаемых цифровыми компьютерами. Языки высокого уровня — это формальные языки , которые строго определены их синтаксисом и семантикой , которые формируют архитектуру языка высокого уровня. Элементы этих формальных языков включают:
Предложения в языке могут быть определены набором правил, называемых грамматикой. [6]
Форма Бэкуса–Наура (БНФ) описывает синтаксис «предложений» языка. Она была разработана Джоном Бэкусом и использовалась для синтаксиса Algol 60. [7] Идеи вытекают из концепций контекстно-свободной грамматики лингвиста Ноама Хомского . [8] «БНФ и ее расширения стали стандартными инструментами для описания синтаксиса программных нотаций. Во многих случаях части компиляторов генерируются автоматически из описания БНФ». [9]
Между 1942 и 1945 годами Конрад Цузе разработал первый (алгоритмический) язык программирования для компьютеров под названием Plankalkül («Плановое исчисление»). Цузе также задумал Planfertigungsgerät («Устройство сборки планов») для автоматического перевода математической формулировки программы в машиночитаемую перфорированную кинопленку . [10] Хотя фактическая реализация не произошла до 1970-х годов, она представила концепции, позже увиденные в APL, разработанном Кеном Айверсоном в конце 1950-х годов. [11] APL — это язык для математических вычислений.
Между 1949 и 1951 годами Хайнц Рутисхаузер предложил Суперплан — язык высокого уровня и автоматический переводчик. [12] Его идеи позднее были усовершенствованы Фридрихом Л. Бауэром и Клаусом Самельсоном . [13]
Разработка языков высокого уровня в годы становления цифровой вычислительной техники предоставила полезные инструменты программирования для различных приложений:
Технология компиляторов развилась из потребности в строго определенном преобразовании исходной программы высокого уровня в целевую программу низкого уровня для цифрового компьютера. Компилятор можно рассматривать как front end для анализа исходного кода и back end для синтеза анализа в целевой код. Оптимизация между front end и back end может производить более эффективный целевой код. [17]
Некоторые ранние вехи в развитии технологии компиляторов:
Ранние операционные системы и программное обеспечение были написаны на языке ассемблера. В 1960-х и начале 1970-х годов использование языков высокого уровня для системного программирования все еще было спорным из-за ограниченности ресурсов. Однако несколько исследовательских и промышленных усилий начали сдвиг в сторону языков системного программирования высокого уровня, например , BCPL , BLISS , B и C.
BCPL (Basic Combined Programming Language), разработанный в 1966 году Мартином Ричардсом в Кембриджском университете, изначально разрабатывался как инструмент для написания компиляторов. [30] Было реализовано несколько компиляторов, книга Ричардса дает представление о языке и его компиляторе. [31] BCPL был не только влиятельным языком системного программирования, который до сих пор используется в исследованиях [32] , но и послужил основой для разработки языков B и C.
BLISS (Basic Language for Implementation of System Software) был разработан для компьютера PDP-10 корпорации Digital Equipment Corporation (DEC) исследовательской группой Университета Карнеги-Меллона (CMU) под руководством У. А. Вульфа. Группа CMU продолжила разработку компилятора BLISS-11 годом позже, в 1970 году.
Multics (Multiplexed Information and Computing Service), проект операционной системы с разделением времени, в котором участвовали MIT , Bell Labs , General Electric (позже Honeywell ) и который возглавлял Фернандо Корбато из MIT. [33] Multics был написан на языке PL/I , разработанном IBM и IBM User Group. [34] Целью IBM было удовлетворение требований делового, научного и системного программирования. Существовали и другие языки, которые можно было бы рассмотреть, но PL/I предлагал наиболее полное решение, хотя оно и не было реализовано. [35] В течение первых нескольких лет проекта Multics подмножество языка можно было скомпилировать в язык ассемблера с помощью компилятора Early PL/I (EPL) Дуга Макилори и Боба Морриса из Bell Labs. [36] EPL поддерживала проект до тех пор, пока не был разработан компилятор для полной загрузки PL/I. [37]
Bell Labs вышла из проекта Multics в 1969 году и разработала системный язык программирования B на основе концепций BCPL, написанный Деннисом Ритчи и Кеном Томпсоном . Ритчи создал компилятор для B и написал операционную систему Unics (Uniplexed Information and Computing Service) для PDP-7 на языке B. В конечном итоге Unics стал писаться как Unix.
Bell Labs начала разработку и расширение C на основе B и BCPL. Компилятор BCPL был перенесен в Multics компанией Bell Labs, и BCPL был предпочтительным языком в Bell Labs. [38] Первоначально использовалась программа front-end для компилятора B Bell Labs, пока разрабатывался компилятор C. В 1971 году новый PDP-11 предоставил ресурс для определения расширений для B и переписывания компилятора. К 1973 году разработка языка C была по существу завершена, и ядро Unix для PDP-11 было переписано на C. Стив Джонсон начал разработку Portable C Compiler (PCC) для поддержки перенацеливания компиляторов C на новые машины. [39] [40]
Объектно-ориентированное программирование (ООП) предлагало некоторые интересные возможности для разработки и обслуживания приложений. Концепции ООП появились гораздо раньше, но были частью науки о языках LISP и Simula . [41] Bell Labs заинтересовалась ООП с разработкой C++ . [42] C++ впервые был использован в 1980 году для системного программирования. Первоначальный проект использовал возможности системного программирования языка C с концепциями Simula. Объектно-ориентированные возможности были добавлены в 1983 году. [43] Программа Cfront реализовала интерфейс C++ для компилятора языка C84. В последующие годы было разработано несколько компиляторов C++ по мере роста популярности C++.
Во многих прикладных областях идея использования языка более высокого уровня быстро прижилась. Из-за расширяющейся функциональности, поддерживаемой новыми языками программирования , и усложнения компьютерных архитектур компиляторы стали более сложными.
DARPA (Управление перспективных исследовательских проектов в области обороны) спонсировало проект компилятора с исследовательской группой Вульфа из CMU в 1970 году. Проект компилятора качества производства PQCC должен был создать компилятор качества производства (PQC) из формальных определений исходного языка и целевого языка. [44] PQCC попытались расширить термин компилятор-компилятор за пределы традиционного значения как генератора синтаксических анализаторов (например, Yacc ), но без особого успеха. PQCC было бы правильнее называть генератором компилятора.
Исследование PQCC в области процесса генерации кода было направлено на создание действительно автоматической системы написания компилятора. В ходе работы была обнаружена и разработана фазовая структура PQC. Компилятор BLISS-11 предоставил начальную структуру. [45] Фазы включали анализ (фронт-энд), промежуточную трансляцию в виртуальную машину (средний конец) и трансляцию в целевой объект (бэк-энд). TCOL был разработан для исследования PQCC для обработки языковых конструкций в промежуточном представлении. [46] Вариации TCOL поддерживали различные языки. Проект PQCC исследовал методы автоматизированного построения компилятора. Концепции проектирования оказались полезными при оптимизации компиляторов и компиляторов для (с 1995 года объектно-ориентированного) языка программирования Ada .
Документ Ada STONEMAN [a] формализовал среду поддержки программ (APSE) вместе с ядром (KAPSE) и минимальным (MAPSE). Интерпретатор Ada NYU/ED поддерживал усилия по разработке и стандартизации совместно с Американским национальным институтом стандартов (ANSI) и Международной организацией по стандартизации (ISO). Первоначальная разработка компилятора Ada военными службами США включала компиляторы в полную интегрированную среду проектирования в соответствии с документом STONEMAN . Армия и флот работали над проектом Ada Language System (ALS), нацеленным на архитектуру DEC/VAX, в то время как ВВС начали работу над Ada Integrated Environment (AIE), нацеленным на серию IBM 370. Хотя проекты не дали желаемых результатов, они внесли вклад в общие усилия по разработке Ada. [47]
Другие усилия по компилятору Ada были предприняты в Великобритании в Университете Йорка и в Германии в Университете Карлсруэ. В США Verdix (позже приобретенная Rational) поставляла Verdix Ada Development System (VADS) для армии. VADS предоставляла набор инструментов разработки, включая компилятор. Unix/VADS могла размещаться на различных платформах Unix, таких как DEC Ultrix и Sun 3/60 Solaris, ориентированная на Motorola 68020 в оценке CECOM армии. [48] Вскоре появилось много доступных компиляторов Ada, которые прошли тесты Ada Validation. Проект Free Software Foundation GNU разработал GNU Compiler Collection (GCC), которая обеспечивает базовую возможность поддержки нескольких языков и целевых платформ. Версия Ada GNAT является одним из наиболее широко используемых компиляторов Ada. GNAT бесплатна, но есть и коммерческая поддержка, например, AdaCore, была основана в 1994 году для предоставления коммерческих программных решений для Ada. GNAT Pro включает в себя GNAT на базе GNU GCC с набором инструментов для предоставления интегрированной среды разработки .
Высокоуровневые языки продолжали стимулировать исследования и разработки компиляторов. Основные направления включали оптимизацию и автоматическую генерацию кода. Тенденции в языках программирования и средах разработки повлияли на технологию компиляторов. Все больше компиляторов стали включаться в языковые дистрибутивы (PERL, Java Development Kit) и как компонент IDE (VADS, Eclipse, Ada Pro). Взаимосвязь и взаимозависимость технологий росли. Появление веб-сервисов способствовало росту веб-языков и языков сценариев. Скрипты восходят к ранним дням интерфейсов командной строки (CLI), где пользователь мог вводить команды для выполнения системой. Концепции оболочки пользователя разрабатывались с языками для написания программ оболочки. Ранние проекты Windows предлагали простую возможность пакетного программирования. Традиционное преобразование этих языков использовало интерпретатор. Хотя компиляторы Bash и Batch не получили широкого распространения, были написаны. Совсем недавно сложные интерпретируемые языки стали частью набора инструментов разработчиков. Современные языки сценариев включают PHP, Python, Ruby и Lua. (Lua широко используется в разработке игр.) Все они имеют поддержку интерпретатора и компилятора. [49]
«Когда в конце 50-х годов началась область компиляции, ее фокус был ограничен переводом программ на языках высокого уровня в машинный код... Область компиляции все больше переплетается с другими дисциплинами, включая архитектуру компьютеров, языки программирования, формальные методы, программную инженерию и компьютерную безопасность». [50] В статье «Исследования компиляторов: следующие 50 лет» отмечалась важность объектно-ориентированных языков и Java. Безопасность и параллельные вычисления были названы среди будущих целей исследований.
Компилятор реализует формальное преобразование из исходной программы высокого уровня в целевую программу низкого уровня. Проектирование компилятора может определять сквозное решение или решать определенную подгруппу, которая взаимодействует с другими инструментами компиляции, например, препроцессорами, ассемблерами, компоновщиками. Требования к проектированию включают строго определенные интерфейсы как внутри между компонентами компилятора, так и снаружи между поддерживающими наборами инструментов.
В ранние дни подход, используемый для проектирования компилятора, напрямую зависел от сложности обрабатываемого компьютерного языка, опыта человека(ов), который(е) его проектировал(и) и доступных ресурсов. Ограничения ресурсов привели к необходимости проходить исходный код более одного раза.
Компилятор для относительно простого языка, написанный одним человеком, может быть единым, монолитным программным обеспечением. Однако по мере усложнения исходного языка проект может быть разделен на ряд взаимозависимых фаз. Отдельные фазы обеспечивают улучшения дизайна, которые фокусируют разработку на функциях в процессе компиляции.
Классификация компиляторов по количеству проходов имеет свою подоплеку в ограничениях аппаратных ресурсов компьютеров. Компиляция подразумевает выполнение большого объема работы, и ранние компьютеры не имели достаточно памяти для одной программы, которая выполняла бы всю эту работу. В результате компиляторы были разделены на более мелкие программы, каждая из которых проходила по исходному коду (или некоторому его представлению), выполняя часть необходимого анализа и трансляций.
Возможность компиляции за один проход традиционно рассматривалась как преимущество, поскольку она упрощает работу по написанию компилятора, а однопроходные компиляторы обычно выполняют компиляцию быстрее, чем многопроходные . Таким образом, отчасти из-за ограничений ресурсов ранних систем, многие ранние языки были специально разработаны так, чтобы их можно было компилировать за один проход (например, Pascal ).
В некоторых случаях дизайн языковой функции может потребовать от компилятора выполнить более одного прохода по исходному коду. Например, рассмотрим объявление, появляющееся в строке 20 исходного кода, которое влияет на перевод оператора, появляющегося в строке 10. В этом случае первый проход должен собрать информацию об объявлениях, появляющихся после операторов, на которые они влияют, а фактический перевод происходит во время последующего прохода.
Недостатком компиляции за один проход является то, что невозможно выполнить многие сложные оптимизации, необходимые для генерации высококачественного кода. Может быть сложно точно подсчитать, сколько проходов делает оптимизирующий компилятор. Например, различные фазы оптимизации могут анализировать одно выражение много раз, но анализировать другое выражение только один раз.
Разделение компилятора на небольшие программы — это метод, используемый исследователями, заинтересованными в создании доказуемо корректных компиляторов. Доказательство корректности набора небольших программ часто требует меньших усилий, чем доказательство корректности более крупной, одиночной, эквивалентной программы.
Независимо от точного количества фаз в конструкции компилятора, фазы могут быть отнесены к одному из трех этапов. Этапы включают в себя front end, middle end и back end.
Такой подход front-middle-back-end позволяет объединять front-end для разных языков с back-end для разных процессоров , при этом разделяя оптимизации middle-end. [51] Практическими примерами такого подхода являются GNU Compiler Collection , Clang ( компилятор C/C++ на основе LLVM ) [52] и Amsterdam Compiler Kit , которые имеют несколько front-end, общие оптимизации и несколько back-end.
Фронтенд анализирует исходный код для построения внутреннего представления программы, называемого промежуточным представлением (IR). Он также управляет таблицей символов , структурой данных, сопоставляющей каждый символ в исходном коде с соответствующей информацией, такой как местоположение, тип и область действия.
Хотя фронтенд может быть единой монолитной функцией или программой, как в парсере без сканера , он традиционно реализовывался и анализировался как несколько фаз, которые могут выполняться последовательно или одновременно. Этот метод пользуется популярностью из-за его модульности и разделения задач . Чаще всего фронтенд разбивается на три фазы: лексический анализ (также известный как лексирование или сканирование), синтаксический анализ (также известный как сканирование или разбор) и семантический анализ . Лексинг и разбор включают синтаксический анализ (синтаксис слова и синтаксис фразы соответственно), и в простых случаях эти модули (лексер и парсер) могут быть автоматически сгенерированы из грамматики языка, хотя в более сложных случаях они требуют ручной модификации. Лексическая грамматика и грамматика фразы обычно являются контекстно-свободными грамматиками , что значительно упрощает анализ, при этом контекстно-зависимость обрабатывается на этапе семантического анализа. Фаза семантического анализа, как правило, более сложная и пишется вручную, но может быть частично или полностью автоматизирована с использованием атрибутных грамматик . Эти фазы сами по себе могут быть далее разбиты: лексический анализ как сканирование и оценка, и парсинг как построение конкретного синтаксического дерева (CST, parse tree) и последующее преобразование его в абстрактное синтаксическое дерево (AST, syntax tree). В некоторых случаях используются дополнительные фазы, в частности, реконструкция строк и предварительная обработка, но они редки.
Основные этапы фронтенда включают в себя следующее:
Средний конец, также известный как оптимизатор, выполняет оптимизацию промежуточного представления с целью повышения производительности и качества создаваемого машинного кода. [56] Средний конец содержит те оптимизации, которые не зависят от целевой архитектуры ЦП.
Основные фазы среднего этапа включают в себя следующее:
Анализ компилятора является предпосылкой для любой оптимизации компилятора, и они тесно работают вместе. Например, анализ зависимости имеет решающее значение для преобразования цикла .
Область анализа и оптимизации компилятора сильно различается; их область действия может варьироваться от работы в пределах базового блока до целых процедур или даже всей программы. Существует компромисс между степенью детализации оптимизаций и стоимостью компиляции. Например, оптимизация peephole выполняется быстро во время компиляции, но затрагивает только небольшой локальный фрагмент кода и может выполняться независимо от контекста, в котором появляется фрагмент кода. Напротив, межпроцедурная оптимизация требует больше времени компиляции и памяти, но позволяет проводить оптимизации, которые возможны только при одновременном рассмотрении поведения нескольких функций.
Межпроцедурный анализ и оптимизация распространены в современных коммерческих компиляторах от HP , IBM , SGI , Intel , Microsoft и Sun Microsystems . Свободное программное обеспечение GCC долгое время критиковалось за отсутствие мощных межпроцедурных оптимизаций, но оно меняется в этом отношении. Еще один компилятор с открытым исходным кодом с полной инфраструктурой анализа и оптимизации — Open64 , который используется многими организациями в исследовательских и коммерческих целях.
Из-за дополнительного времени и пространства, необходимых для анализа и оптимизации компилятора, некоторые компиляторы пропускают их по умолчанию. Пользователи должны использовать параметры компиляции, чтобы явно указать компилятору, какие оптимизации следует включить.
Бэкэнд отвечает за оптимизацию архитектуры ЦП и генерацию кода [56] .
Основные этапы бэкэнда включают в себя следующее:
Корректность компилятора — это раздел программной инженерии, который занимается попытками показать, что компилятор ведет себя в соответствии со спецификацией своего языка . [58] Методы включают разработку компилятора с использованием формальных методов и использование строгого тестирования (часто называемого проверкой компилятора) на существующем компиляторе.
Языки программирования более высокого уровня обычно появляются с учетом типа трансляции : либо разработанные как компилируемый язык , либо интерпретируемый язык . Однако на практике редко бывает так, чтобы язык требовал исключительно компилируемого или исключительно интерпретируемого языка, хотя можно разрабатывать языки, которые полагаются на повторную интерпретацию во время выполнения. Категоризация обычно отражает наиболее популярные или распространенные реализации языка — например, BASIC иногда называют интерпретируемым языком, а C — компилируемым, несмотря на существование компиляторов BASIC и интерпретаторов C.
Интерпретация не заменяет компиляцию полностью. Она только скрывает ее от пользователя и делает ее постепенной. Несмотря на то, что интерпретатор сам по себе может быть интерпретирован, необходим набор непосредственно выполняемых машинных инструкций где-то в нижней части стека выполнения (см. машинный язык ).
Кроме того, для оптимизации компиляторы могут содержать функционал интерпретатора, а интерпретаторы могут включать методы компиляции до начала работы. Например, когда выражение может быть выполнено во время компиляции, а результаты вставлены в выходную программу, то это предотвращает необходимость пересчета при каждом запуске программы, что может значительно ускорить окончательную программу. Современные тенденции к компиляции «точно в срок» и интерпретации байт-кода порой еще больше размывают традиционные классификации компиляторов и интерпретаторов.
Некоторые спецификации языка указывают, что реализации должны включать средство компиляции; например, Common Lisp . Однако в определении Common Lisp нет ничего, что мешало бы его интерпретировать. В других языках есть функции, которые очень легко реализовать в интерпретаторе, но которые значительно усложняют написание компилятора; например, APL , SNOBOL4 и многие скриптовые языки позволяют программам создавать произвольный исходный код во время выполнения с помощью обычных строковых операций, а затем выполнять этот код, передавая его специальной функции оценки . Чтобы реализовать эти функции в компилируемом языке, программы обычно должны поставляться с библиотекой времени выполнения , которая включает версию самого компилятора.
Одна из классификаций компиляторов — по платформе, на которой выполняется сгенерированный ими код. Это называется целевой платформой.
Собственный или размещенный компилятор — это тот, чей вывод предназначен для непосредственного запуска на том же типе компьютера и операционной системы, на которых работает сам компилятор. Вывод кросс-компилятора предназначен для запуска на другой платформе. Кросс-компиляторы часто используются при разработке программного обеспечения для встраиваемых систем , которые не предназначены для поддержки среды разработки программного обеспечения.
Вывод компилятора, который производит код для виртуальной машины (VM), может быть выполнен или не выполнен на той же платформе, что и компилятор, который его создал. По этой причине такие компиляторы обычно не классифицируются как собственные или кросс-компиляторы.
Язык более низкого уровня, который является целью компилятора, сам по себе может быть языком программирования высокого уровня . C, рассматриваемый некоторыми как своего рода переносимый язык ассемблера, часто является целевым языком таких компиляторов. Например, Cfront , оригинальный компилятор для C++ , использовал C в качестве своего целевого языка. Код C, сгенерированный таким компилятором, обычно не предназначен для чтения и поддержки людьми, поэтому стиль отступов и создание красивого промежуточного кода C игнорируются. Некоторые из особенностей C, которые делают его хорошим целевым языком, включают #line
директиву, которая может быть сгенерирована компилятором для поддержки отладки исходного кода, и широкую поддержку платформ, доступную с компиляторами C.
Хотя распространенный тип компилятора выводит машинный код, существует множество других типов:
DOALL
). Другие термины для компилятора исходного кода — транскомпилятор или транспилятор. [59]Ассемблеры, которые переводят понятный человеку язык ассемблера в инструкции машинного кода , выполняемые аппаратным обеспечением, не считаются компиляторами. [66] [b] (Обратная программа, которая переводит машинный код на язык ассемблера, называется дизассемблером . )
Компилятор — это компьютерная программа, которая переводит программу, написанную на языке высокого уровня (HLL), например C, в эквивалентную программу на языке ассемблера [2].
{{cite book}}
: |website=
проигнорировано ( помощь )Первый текст по построению компилятора.