Ответ на вопрос
Компилятор
Компиля́тор — программа, переводящая программу с исходного языка в целевой, например в машинный код или байткод[1]. При предварительной компиляции перевод выполняется до запуска[2]; при динамической компиляции (JIT) код генерируется во время выполнения[3]. Интерпретатор исполняет программу или её промежуточное представление. Компилятор входит в состав систем программирования вместе с редактором, отладчиком и средствами сборки, а его качество напрямую определяет, насколько быстро будет работать готовая программа[4].
История компиляторов началась в 1952 году, когда Грейс Хоппер написала программу А-0, автоматически подставлявшую подпрограммы в код для ЭВМ UNIVAC (в то время термин «компилятор» означал скорее библиотечную сборку, чем современный транслятор языка)[5]. С тех пор компиляция стала промышленной технологией: на ней держатся операционные системы, системы управления базами данных и браузеры, а свободные наборы компиляторов GCC и LLVM перевели её в разряд общедоступной инфраструктуры[6]. Современные среды исполнения объединяют компиляцию, интерпретацию и динамическую компиляцию (just-in-time) в одной системе, поэтому граница между стратегиями перевода постепенно размывается[7].
Компилятор — не только переводчик, но и анализатор: он находит сотни ошибок до первого запуска программы, от опечаток в именах до нарушения типов. Доверие к нему имеет обратную сторону: заражённый компилятор способен внедрять закладки в каждую собранную им программу, поэтому в компьютерной безопасности компиляторы рассматривают как звено цепочки поставки программного обеспечения[8].
Общие сведения
| Компилятор | |
|---|---|
| Область использования | перевод программ с исходного языка программирования в целевое представление |
| Дата появления | 1952 |
| Место появления | США (ЭВМ UNIVAC) |
| Автор понятия | Грейс Хоппер (первый компилятор А-0) |
| Ключевые слова | язык программирования, байткод, оптимизация кода, генерация целевого кода |
| Базовые понятия | лексический анализ, синтаксический анализ, семантический анализ, байткод, оптимизация кода |
История
Первые компиляторы
К началу 1950-х годов программы писали непосредственно в машинных кодах, а типовые вычисления переписывали из тетрадей вручную. Грейс Хоппер предложила поручить эту работу самой машине: её система А-0 выбирала нужные подпрограммы из библиотеки по символьным меткам и собирала из них готовую программу[5]. Хоппер назвала такую обработку «автоматическим программированием» и подчёркивала, что перевод освобождает программиста от знания команд конкретной машины[5]. Вслед за А-0 появились развитые версии её системы для машин UNIVAC, а сама Хоппер настаивала, что вычислительная машина должна понимать запись, близкую к обычной математике[9].
Следующий шаг сделал коллектив под руководством Джона Бэкуса, создавший к 1957 году компилятор Фортрана для машины IBM 704. Заказчики сомневались, что автоматический перевод окупится: ручное кодирование считалось неизбежным злом. Проект доказал обратное: скомпилированные программы работали достаточно эффективно, а затраты на разработку компилятора быстро окупились[10]. После этого языки высокого уровня стали основным способом записи программ, а компилятор — обязательной частью вычислительной платформы[9].
Формальные основы
Стихийные приёмы перевода быстро упёрлись в сложность языков. Отчёт об АЛГОЛЕ 60, подготовленный группой Питера Наура, закрепил описание синтаксиса языка в форме Бэкуса — Наура — формальной грамматике, порождающей правильные программы[11]. Ещё в 1956 году Ноам Хомский описал иерархию формальных грамматик, задавшую теоретический каркас разбора языков[12]. С этого момента построение компилятора превратилось из ремесла в инженерную дисциплину с проверяемой теорией: грамматика служит и определением языка, и схемой работы синтаксического анализатора[1].
Инструменты подхватили теорию почти сразу: генераторы синтаксических анализаторов строят программу-анализатор по формальному описанию грамматики, поэтому новая версия языка подхватывается перекомпиляцией описания, а не переписыванием анализатора вручную[1]. Та же участь постигла и лексику: описания токенов в терминах регулярных выражений превращаются генератором в таблицы конечных автоматов[2]. Благодаря этому значительная часть компилятора собирается из готовых компонентов, а усилия разработчиков сосредоточены на семантике языка и качестве генерируемого кода[13].
Оптимизация и переносимость
В 1960—1970-е годы центр тяжести сместился к качеству порождаемого кода. Коллектив Фрэнсис Аллен в компании IBM развил теорию анализа потоков данных, на которой держатся современные оптимизирующие компиляторы; сама Аллен стала первой женщиной — лауреатом Премии Тьюринга в 2006 году[14]. В СССР под руководством Андрея Ершова создавался Альфа-транслятор для М-20, ориентированный на получение эффективного объектного кода. При переходе к БЭСМ-6 был разработан кросс-транслятор Алгибр[15].
Идея единого компилятора для разных процессоров породила многоязычные и переносимые системы. В 1984 году Дойч и Шиффман показали, что даже динамический язык вроде Smalltalk-80 можно ускорить переводом «горячих» методов в машинный код прямо во время работы программы, — так оформилась динамическая компиляция[16]. Первый бета-выпуск свободного компилятора GCC (0.9) вышел 22 марта 1987 года, а версия 1.0 — 23 мая 1987 года, закрепив за GNU роль центра открытых средств разработки[17]. Инфраструктура LLVM, представленная Латтнером и Адве в 2004 году, довела переносимость до логического конца: единый промежуточный код отделил языки от процессоров, и под него стали строиться новые языки и среды[18].
Устройство компилятора
Классическую схему компилятора описывают как конвейер фаз, каждая из которых получает представление программы, преобразует его и передаёт следующей[1]. Первые фазы — лексический, синтаксический и семантический анализ — собирают из текста проверенную модель программы и зависят от исходного языка. Вторая половина конвейера — построение промежуточного представления, оптимизация (как машинно-независимая, так и машинно-зависимая), генерация кода и распределение регистров — отвечает за адаптацию к целевой платформе[13]. Части конвейера вокруг анализа исходного текста называют передней частью компилятора (фронтенд), а вокруг генерации команд — задней (бэкенд); такое разделение позволяет добавлять новые языки и новые процессоры, не переписывая компилятор целиком[4].
Лексический анализ
Лексический анализатор читает текст программы слева направо и разрезает его на минимальные значимые единицы — токены: имена, ключевые слова, числовые литералы, знаки операций. Обработка пробелов и переводов строк зависит от языка. Например, в Python отступы и окончания логических строк участвуют в формировании токенов INDENT, DEDENT и NEWLINE[19]. Сведения об идентификаторах используются при дальнейшем анализе имён и областей видимости.[1] Работа лексера опирается на регулярные выражения и конечные автоматы: правила выделения токенов описываются регулярными выражениями, и по ним строится конечный автомат[2]. Если очередной символ не образует допустимого токена, анализатор немедленно сообщает о лексической ошибке с указанием позиции[13].
Синтаксический анализ
Синтаксический анализатор, или парсер, получает поток токенов и проверяет, образуют ли они правильную программу с точки зрения грамматики языка; результатом становится дерево разбора, отражающее вложенность конструкций[1]. Выражение в правой части присваивания получает корень в знаке операции, а операнды становятся его потомками, поэтому порядок вычислений выводится из структуры дерева, хотя фактический порядок вычисления операндов и побочных эффектов зависит от семантики конкретного языка[13]. Практические анализаторы строятся по методам рекурсивного спуска либо табличных алгоритмов LR-семейства; выбор метода определяет класс допустимых грамматик и удобство сообщений об ошибках[13]. Обнаружив недопустимую конструкцию, парсер локализует её и продолжает разбор, чтобы собрать программисту несколько замечаний за один прогон[2].
Семантический анализ и промежуточное представление
Правильная по грамматике программа может быть бессмысленной: переменная не объявлена, типы несовместимы, функция вызвана с неверным числом аргументов. Эти проверки выполняет семантический анализатор, который обходит дерево разбора, сверяется с таблицей символов и дополняет узлы сведениями о типах[1]. Проверенная программа переводится в промежуточное представление — внутренний язык компилятора, достаточно низкий, чтобы выражать команды процессоров, и достаточно высокий, чтобы сохранять структуру программы[4]. Классической формой служит трёхадресный код, в котором каждая операция имеет не более одного результата; распространены и представление в виде ациклического графа, и форма единственного присваивания, облегчающая оптимизацию[20]. Именно единое промежуточное представление позволило связать в одном конвейере десятки языков и десятки целевых архитектур — так устроены GCC и LLVM[18].
Обработка ошибок и диагностика
Практический компилятор обязан работать с ошибочным текстом так, чтобы за один прогон сообщить о как можно большем числе проблем. Для этого используется восстановление после ошибки: анализатор локализует повреждённую конструкцию, отбрасывает её часть и продолжает разбор со следующего пункта, не позволяя одной ошибке обрушить всю трансляцию[1]. Простейшая стратегия — панический режим, при котором входные символы пропускаются до синхронизирующего ключевого слова или знака; языки со структурными разделителями позволяют восстанавливаться точнее[13].
Качество диагностик определяет удобство языка не меньше, чем скорость готового кода: сообщение должно указывать позицию ошибки, предполагаемую причину и место связанного объявления[2]. Особую сложность составляют каскадные ошибки, когда одна неточность порождает целую серию вторичных сообщений, исчезающих после первого исправления[2]. Поэтому типовые ситуации — пропущенная точка с запятой, несовпадение числа аргументов, несовместимость типов при присваивании — стараются описывать в терминах исходной программы, а не внутреннего представления компилятора[1].
Оптимизация и генерация кода
Классы преобразований
Оптимизацией называют безопасные преобразования программы, сохраняющие её видимый результат, но сокращающие время работы или размер кода[20]. Локальные преобразования действуют внутри линейных участков: свёртка констант подставляет значения известных выражений, понижение силы операций заменяет умножение сдвигами, избыточные пересылки удаляются[4]. Внутрипроцедурные преобразования анализируют граф потока управления всей функции: они удаляют вычисления, результат которых не используется, разворачивают короткие циклы и переносят инвариантные вычисления за их границы[20]. Межпроцедурный анализ охватывает программу целиком и позволяет встраивать тела функций вместо вызовов, открывая дальнейшие оптимизации на стыке процедур[4].
Распределение регистров и генерация кода
Задняя часть компилятора превращает оптимизированное представление в команды конкретного процессора. Выбор инструкций подбирает для каждой операции эквивалентные машинные команды, сопоставляя их стоимость; планирование переупорядочивает команды, заполняя задержки конвейера[4]. Распределение регистров назначает переменные ограниченному набору регистров процессора; классическое решение — раскраска графа несовместимости, где цвета соответствуют регистрам[20]. Не уместившиеся значения вытесняются в память, и качество этого шага заметно влияет на скорость готового кода[4]. Компиляторы применяются и за пределами обычной трансляции: например, система SWIFT вставляет в код избыточные вычисления и проверки для обнаружения и маскировки аппаратных сбоев в суперкомпьютерных программах[21].
Уровни оптимизации
Программист управляет глубиной преобразований через уровни оптимизации: сборка без оптимизации обычно проходит быстрее и может упрощать отладку, а высшие уровни включают более сложные проходы анализа и могут заметно удлинять трансляцию[22]. Уровень подбирают по назначению сборки: библиотеки могут собирать с контролем размера кода, вычислительно ёмкие программы — с более агрессивной оптимизацией[22]. Отдельные ключи отключают преобразования, способные изменить наблюдаемое поведение программы: например, в GCC флаг -fwrapv задаёт циклическое переполнение знаковых операций, что требует явного указания, если код полагается на такое поведение[20].
Стоимость проходов вынуждает выстраивать их порядок: дешёвые локальные преобразования выполняются на каждом уровне, а дорогие формы анализа включаются только по явному запросу[4]. Прирост от высших уровней насыщается: часть преобразований дублирует друг друга, а последние проценты скорости достигаются машинно-зависимой настройкой под конкретный процессор[20].
Динамическая компиляция
Конкретная стратегия зависит от среды исполнения. Например, CPython использует собственный байткод, формат которого не гарантированно совместим между версиями и реализациями.[23] В движке JavaScript V8 применяется интерпретатор Ignition, работающий с байткодом; интерпретация может сочетаться с компиляцией в машинный код[24]. Такая динамическая компиляция опирается на предположения, накопленные при наблюдении за программой: если типы значения изменились, скомпилированный код отбрасывается и исполнение возвращается к интерпретатору[7]. Приём восходит к системе Smalltalk-80, где перевод методов в машинный код на ходу ускорил исполнение в десятки раз[16]. Современные виртуальные машины JavaScript компилируют один и тот же код несколькими уровнями оптимизации, постепенно повышая качество там, где это окупается[25].
Компиляция и интерпретация
Сравнение стратегий
Предварительная компиляция переносит перевод кода на этап до выполнения; динамическая компиляция выполняет его во время работы программы.[3] Интерпретатор может исполнять заранее полученное промежуточное представление, например байткод CPython, поэтому повторный разбор исходного текста при каждом выполнении не обязателен[23].
Никлаус Вирт отмечал, что сам язык можно проектировать так, чтобы компиляция выполнялась за один проход и была быстрой даже на скромной машине[26].
Гибридные среды исполнения
Среды исполнения используют разные комбинации механизмов. В типичном JIT-сценарии .NET код CIL компилируется в машинный при вызове метода; обязательный предварительный этап интерпретации для этого не требуется[27]. Учебные системы идут по пути упрощения: транслятор проекта РуСи переводит программу на Си в коды учебной виртуальной машины, что позволяет студентам работать с единым и понятным исполнением на разных платформах[28]. Способ поставки программы сам по себе не определяет стратегию её исполнения: среда может сочетать интерпретацию и компиляцию.[3][23]
Ограничения и злоупотребления
Атаки через доверие к компилятору
Кен Томпсон показал, что компилятор можно научить внедрять закладку: заражённый транслятор добавляет скрытый вход в компилируемые программы и заодно копирует сам себя в свои новые сборки, так что закладка переживает замену исходного текста[8]. Обнаружить такую модификацию чтением исходников невозможно — нужен контроль над всем конвейером сборки. Пример практической атаки показал вирус XcodeGhost в сентябре 2015 года: разработчики приложений для iOS собрали свои программы заражённой копией среды разработки, и вредоносный код попал в ряд приложений официального магазина[29].
Воспроизводимые сборки
Воспроизводимой называют сборку, при которой одинаковые исходный код, окружение и инструкции позволяют независимо получить побитово совпадающие артефакты[30]. Такая воспроизводимость сама по себе не доказывает отсутствие вредоносного кода в исходниках или используемом компиляторе.
Дэвид А. Уилер описал метод двойного разнообразного компилирования (DDC). Сначала доверенным компилятором компилируют исходный код компилятора-родителя, затем полученным компилятором — исходный код проверяемого компилятора. При выполнении предпосылок метода побитовое совпадение результата с проверяемым исполняемым файлом подтверждает соответствие этого файла исходному коду[31]. Движение воспроизводимых сборок переносит ту же идею на промышленные дистрибутивы: пакет собирается в описанной среде, и любой может повторить процесс побитово, что делает скрытые вставки уязвимыми к проверке[32]. Воспроизводимость уже стала частью практик крупных проектов и дистрибутивов, а её отсутствие фиксируется как дефект сборки[33].
Обфускация и защита программ
Те же механизмы трансформации, что ускоряют программу, пригодны и для её защиты. Колберг и Торборсон формализовали обфускацию, водяные знаки и защиту от изменения как преобразования программы: первое затрудняет понимание кода, вторая помогает в установлении авторства, третья мешает несанкционированным модификациям[34]. Силу защиты ограничивает сама природа компиляции: из работающей программы извлекается наблюдаемое поведение, поэтому обфускация лишь повышает стоимость анализа, не делая его невозможным[34]. Обратная задача — восстановление исходного представления по машинному коду — решается декомпиляторами и применяется при анализе вредоносного кода и проверке унаследованного кода[13].
Связь с другими понятиями
Компилятор — частный случай транслятора: переводом программ занимаются также ассемблеры, переводящие мнемоники команд в машинный код почти один к одному, и препроцессоры, подготавливающие текст к основной обработке[1]. К семейству трансляторов принадлежит и транспилятор — переводчик между языками одного уровня. Такие системы применяют, например, для автоматической вставки в исходный код средств контроля ошибок, после которой дополненный код проходит обычную сборку[35]. От декомпилятора компилятор отличается направлением перевода и полнотой: обратный путь восстанавливает структуру программы приблизительно, теряя имена и комментарии[13]. Кросс-компилятор строит код для платформы, отличной от той, где работает сам, и потому служит основным инструментом разработки для встраиваемых систем и мобильных устройств[4].
Компиляция конкурирует и соединяется с интерпретацией, а скорость порождаемого кода определяется не только транслятором, но и средой исполнения — библиотеками, сборщиком мусора, виртуальной машиной[2]. Проектирование языка и проектирование компилятора связаны двусторонне: конструкции, удобные для человека, заставляют усложнять анализ, а возможности процессоров подталкивают языки к новым формам[26]. Поэтому теория формальных языков, системы типов и архитектура процессоров образуют общий фундамент, на котором компилятор остаётся главным мостом между замыслом программиста и работой машины[1].
Примечания
- ↑ 1 2 3 4 5 6 7 8 9 10 11 Aho A. V., Lam M. S., Sethi R., Ullman J. D. Compilers: principles, techniques, and tools. — 2nd ed.. — Addison-Wesley, 2006. — 1000 с. — ISBN 9780321486813.
- ↑ 1 2 3 4 5 6 7 Louden K. C. Compiler construction: principles and practice. — PWS Pub. Co., 1997. — 582 с. — ISBN 9780534939724.
- ↑ 1 2 3 Kaleidoscope: Adding JIT and Optimizer Support (англ.). LLVM Project. Дата обращения: 2 октября 2026.
- ↑ 1 2 3 4 5 6 7 8 9 Cooper K. D., Torczon L. Engineering a compiler. — 2nd ed.. — Elsevier/Morgan Kaufmann, 2012. — 800 с. — ISBN 9780120884780.
- ↑ 1 2 3 Hopper G. M. The education of a computer // Proceedings of the 1952 ACM national meeting (Pittsburgh). — 1952. — С. 243—249. — doi:10.1145/609784.609818.
- ↑ The LLVM Compiler Infrastructure Project (англ.). llvm.org. LLVM Project (2026).
- ↑ 1 2 Чилингарова С. А. Методы оптимизации для динамических (just-in-time) компиляторов. Часть 2. Примеры современных динамических компиляторов // Компьютерные инструменты в образовании. — 2006. — № 3.
- ↑ 1 2 Thompson K. Reflections on trusting trust // Communications of the ACM. — 1984. — Т. 27, № 8. — С. 761—763. — doi:10.1145/358198.358210.
- ↑ 1 2 Allen F. E. The History of Language Processor Technology in IBM // IBM Journal of Research and Development. — 1981. — Т. 25, № 5. — С. 535—548. — doi:10.1147/rd.255.0535.
- ↑ Backus J. Can programming be liberated from the von Neumann style? A functional style and its algebra of programs // Communications of the ACM. — 1978. — Т. 21, № 8. — С. 613—641. — doi:10.1145/359576.359579.
- ↑ Backus J. W., Bauer F. L., Green J. и др. Report on the algorithmic language ALGOL 60 // Communications of the ACM. — 1960. — Т. 3, № 5. — С. 299—311. — doi:10.1145/367236.367262.
- ↑ Chomsky N. Three models for the description of language // IEEE Transactions on Information Theory. — 1956. — Т. 2, № 3. — С. 113—124. — doi:10.1109/tit.1956.1056813.
- ↑ 1 2 3 4 5 6 7 8 Grune D., van Reeuwijk K., Bal H. E., Jacobs C. J. H., Langendoen K. Modern compiler design. — 2nd ed.. — Springer, 2012. — 843 с. — ISBN 9781461446989.
- ↑ Frances E. Allen (англ.). ACM. Дата обращения: 2 октября 2026.
- ↑ Поттосин И. В. А. П. Ершов и становление новосибирской школы программирования. ИСИ СО РАН. Дата обращения: 2 октября 2026.
- ↑ 1 2 Deutsch L. P., Schiffman R. M. Efficient implementation of the smalltalk-80 system // Proceedings of the 11th ACM SIGACT-SIGPLAN symposium on Principles of programming languages. — 1984. — С. 297—302. — doi:10.1145/800017.800542.
- ↑ GCC Releases - GNU Project (англ.). gnu.org. Free Software Foundation (2026).
- ↑ 1 2 Lattner C., Adve V. LLVM: a compilation framework for lifelong program analysis & transformation // Proceedings of the International Symposium on Code Generation and Optimization. — 2004. — С. 75—86. — doi:10.1109/cgo.2004.1281665.
- ↑ Lexical analysis (англ.). Python Software Foundation. Дата обращения: 2 октября 2026.
- ↑ 1 2 3 4 5 6 Muchnick S. S. Advanced compiler design and implementation. — Morgan Kaufmann, 1997. — 856 с. — ISBN 9781558603202.
- ↑ Reis G. A., Chang P. M., Vachharajani N., Rangan R., August D. I. SWIFT: Software Implemented Fault Tolerance // Proceedings of the International Symposium on Code Generation and Optimization. — 2005. — С. 243—254. — doi:10.1109/cgo.2005.34.
- ↑ 1 2 Optimize Options (Using the GNU Compiler Collection (GCC)) (англ.). gcc.gnu.org. Free Software Foundation (2026).
- ↑ 1 2 3 dis — Disassembler for Python bytecode (англ.). Python Software Foundation. Дата обращения: 2 октября 2026.
- ↑ Ignition (англ.). V8 Project. Дата обращения: 2 октября 2026.
- ↑ Варданян В. Г., Иванишин В. А., Асрян С. А. и др. Динамическая компиляция программ на языке JavaScript в статически типизированное внутреннее представление LLVM // Труды Института системного программирования РАН. — 2015. — Т. 27, № 6.
- ↑ 1 2 Wirth N. Compiler construction. — Addison-Wesley, 1996. — 176 с. — ISBN 9780201403534.
- ↑ Managed Execution Process (англ.). Microsoft. Дата обращения: 2 октября 2026.
- ↑ Терехов А. Н., Терехов М. А. Проект РуСи для обучения и создания высоконадежных программных систем // Известия высших учебных заведений. Северо-Кавказский регион. Технические науки. — 2017. — № 3 (195).
- ↑ Novel Malware XcodeGhost Modifies Xcode, Infects Apple iOS Apps and Hits App Store (англ.). unit42.paloaltonetworks.com. Palo Alto Networks (2015).
- ↑ Definitions (англ.). Reproducible Builds. Дата обращения: 2 октября 2026.
- ↑ Fully Countering Trusting Trust through Diverse Double-Compiling (DDC) - Countering Trojan Horse attacks on Compilers (англ.). dwheeler.com. D. A. Wheeler.
- ↑ Lamb C., Zacchiroli S. Reproducible Builds: Increasing the Integrity of Software Supply Chains // IEEE Software. — 2022. — Т. 39, № 2. — С. 62—70. — doi:10.1109/ms.2021.3073045.
- ↑ Reproducible Builds — a set of software development practices that create an independently-verifiable path from source to binary code (англ.). reproducible-builds.org (2026).
- ↑ 1 2 Collberg C., Thomborson C. Watermarking, tamper-proofing, and obfuscation - tools for software protection // IEEE Transactions on Software Engineering. — 2002. — Т. 28, № 8. — С. 735—746. — doi:10.1109/tse.2002.1027797.
- ↑ Rebaudengo M., Reorda M. S., Violante M., Torchiano M. A source-to-source compiler for generating dependable software // Proceedings of the First IEEE International Workshop on Source Code Analysis and Manipulation (SCAM). — 2001. — С. 33—42. — doi:10.1109/scam.2001.972664.
Литература
- Варданян В. Г., Иванишин В. А., Асрян С. А. и др. Динамическая компиляция программ на языке JavaScript в статически типизированное внутреннее представление LLVM // Труды Института системного программирования РАН. — 2015. — Т. 27, № 6.
- Терехов А. Н., Терехов М. А. Проект РуСи для обучения и создания высоконадежных программных систем // Известия высших учебных заведений. Северо-Кавказский регион. Технические науки. — 2017. — № 3 (195).
- Чилингарова С. А. Методы оптимизации для динамических (just-in-time) компиляторов. Часть 2. Примеры современных динамических компиляторов // Компьютерные инструменты в образовании. — 2006. — № 3.
- Aho A. V., Lam M. S., Sethi R., Ullman J. D. Compilers: principles, techniques, and tools. — 2nd ed.. — Addison-Wesley, 2006. — 1000 с. — ISBN 9780321486813.
- Backus J. Can programming be liberated from the von Neumann style? A functional style and its algebra of programs // Communications of the ACM. — 1978. — Т. 21, № 8. — С. 613—641. — doi:10.1145/359576.359579.
- Backus J. W., Bauer F. L., Green J. и др. Report on the algorithmic language ALGOL 60 // Communications of the ACM. — 1960. — Т. 3, № 5. — С. 299—311. — doi:10.1145/367236.367262.
- Cooper K. D., Torczon L. Engineering a compiler. — 2nd ed.. — Elsevier/Morgan Kaufmann, 2012. — 800 с. — ISBN 9780120884780.
- Grune D., van Reeuwijk K., Bal H. E., Jacobs C. J. H., Langendoen K. Modern compiler design. — 2nd ed.. — Springer, 2012. — 843 с. — ISBN 9781461446989.
- Hopper G. M. The education of a computer // Proceedings of the 1952 ACM national meeting (Pittsburgh). — 1952. — С. 243—249. — doi:10.1145/609784.609818.
- Thompson K. Reflections on trusting trust // Communications of the ACM. — 1984. — Т. 27, № 8. — С. 761—763. — doi:10.1145/358198.358210.