Вопрос:
«Некоторые свойства языка программирования могут требовать, чтобы единственный способ получить код, написанный на нем, выполнялся путем интерпретации. Другими словами, компиляция в собственный машинный код традиционного ЦП невозможна. Что это за свойства?»
Составители: принципы и практика Параг Х. Дейва и Химаншу Б. Дейва (2 мая 2012 г.)
Книга не дает подсказки об ответе. Я пытался найти ответ на Концепции языков программирования (SEBESTA), но безрезультатно. Поиски в Интернете тоже мало что дали. Есть ли у вас какие-либо подсказки?
programming-languages
compilers
interpreters
Андерсон Насименто Нунес
источник
источник
Ответы:
Различие между интерпретируемым и скомпилированным кодом, вероятно, является фикцией, как подчеркивается в комментарии Рафаэля :
Дело в том, что код всегда интерпретируется программным обеспечением, аппаратным обеспечением или их комбинацией, и процесс компиляции не может определить, каким он будет.
То, что вы воспринимаете как компиляцию, - это процесс перевода с одного языка (для источника) на другой язык T (для цели). И, интерпретатор для S , как правило , отличается от переводчика для T .S T S T
Скомпилированная программа переводится из одной синтаксической формы в другую синтаксическую форму P T , так что, учитывая предполагаемую семантику языков S и T , P S и P T имеют одинаковое вычислительное поведение, вплоть до нескольких вещей, которые вы обычно пытаются изменить, возможно, оптимизировать, например, сложность или простую эффективность (время, пространство, поверхность, потребление энергии). Я стараюсь не говорить о функциональной эквивалентности, поскольку это потребовало бы точных определений.PS PT S T PS PT
Некоторые компиляторы фактически использовались просто для уменьшения размера кода, а не для «улучшения» исполнения. Это имело место для языка, используемого в системе Платона (хотя они не называли это компиляцией).
Вы можете рассмотреть ваш код полностью составлен , если после процесса компиляции, вам больше не нужен переводчик для . По крайней мере, это единственный способ, которым я могу прочитать ваш вопрос, как технический, а не теоретический вопрос (поскольку, теоретически, я всегда могу перестроить переводчика).S
Afaik, одна вещь, которая может создать проблему, это мета-цикличность . То есть, когда программа будет манипулировать синтаксическими структурами на своем собственном языке , создавая фрагмент программы, который затем интерпретируется так, как если бы он был частью исходной программы. Поскольку вы можете создавать произвольные фрагменты программы на языке S в результате произвольных вычислений, манипулирующих бессмысленными синтаксическими фрагментами, я думаю, вы можете сделать почти невозможным (с инженерной точки зрения) компиляцию программы в язык T , так что теперь генерировать фрагменты Т . Следовательно, потребуется интерпретатор для S или, по крайней мере, компилятор из S вS S T T S S дляоперативной компиляциисгенерированных фрагментов в S (см. Такжеэтот документ).T S
Но я не уверен, как это можно правильно оформить (и сейчас у меня нет на это времени). И невозможно это большое слово для вопроса, который не формализован.
Дальнейшие замечания
Добавлено через 36 часов. Вы можете пропустить это очень долгое продолжение.
Многочисленные комментарии к этому вопросу показывают два взгляда на проблему: теоретический взгляд, который рассматривает ее как бессмысленный, и инженерный взгляд, который, к сожалению, не так легко формализовать.
Есть много способов взглянуть на интерпретацию и компиляцию, и я постараюсь сделать несколько набросков. Я постараюсь быть настолько неформальным, насколько я могу управлять
Схема надгробия
Одной из ранних формализаций (с начала 1960-х до конца 1990-х годов) являются диаграммы T или Tombstone . Эти диаграммы представлены в виде компонуемых графических элементов, языка реализации интерпретатора или компилятора, исходного языка, который интерпретируется или компилируется, и целевого языка в случае компиляторов. Более сложные версии могут добавлять атрибуты. Эти графические представления можно рассматривать как аксиомы, правила вывода, которые можно использовать для механического получения генерации процессоров из доказательства их существования по аксиомам, как Карри-Ховард (хотя я не уверен, что это было сделано в шестидесятых годах :).
Частичная оценка
Другой интересный взгляд - это парадигма частичной оценки . Я просто рассматриваю программы как своего рода реализацию функций, которая вычисляет ответ с учетом некоторых входных данных. Затем интерпретатор для языка S представляет собой программу, взять программу р S , написанный на S и данных D для этой программы, и вычисляет результат в соответствии с семантикой S . Частичная оценка представляет собой метод , специализирующуюся программу двух аргументов 1 и 2 , когда только один аргумент, скажем , с 1IS S pS S d S a1 a2 a1 , известен. Намерение состоит в том, чтобы иметь более быструю оценку, когда вы, наконец, получите второй аргумент . Это особенно полезно , если 2 изменяется чаще , чем в 1 , как стоимость частичной оценки с более 1 может быть амортизируется всех вычислений , где только 2 меняется.a2 a2 a1 a1 a2
Это частая ситуация при разработке алгоритмов (часто это тема первого комментария к SE-CS), когда некоторая более статическая часть данных предварительно обрабатывается, так что стоимость предварительной обработки может быть амортизирована во всех приложениях. алгоритма с более переменными частями входных данных.
Это также является самой ситуацией интерпретаторов, так как первый аргумент - это программа, которая должна быть выполнена, и обычно выполняется много раз с разными данными (или имеет части, выполненные много раз с разными данными). Следовательно, стало естественной идеей специализировать интерпретатора для более быстрой оценки данной программы, частично оценивая его в этой программе в качестве первого аргумента. Это можно рассматривать как способ компиляции программы, и была проведена значительная исследовательская работа по компиляции путем частичной оценки интерпретатора по его первому (программному) аргументу.
Теорема Smn
Приятный момент в подходе частичной оценки состоит в том, что он берет свои корни в теории (хотя теория может быть лжецом), особенно в теореме Клини о Смне . Я пытаюсь дать интуитивное представление об этом, надеясь, что это не расстроит чисто теоретиков.
С учетом нумерации Гёделя рекурсивных функций вы можете рассматривать φ как свое аппаратное обеспечение, так что с учетом числа Геделя p (считывание объектного кода ) программы φ p является функцией, определенной как p (то есть вычисляемой объектным кодом на вашем оборудовании ).φ φ p φp p
В простейшем виде теорема изложена в википедии следующим образом (с небольшим изменением в обозначениях):
Теперь, взяв в качестве интерпретатора I S , x в качестве исходного кода программы p S и y в качестве данных d для этой программы, мы можем написать:Q яS Икс пS Y d φσ( ЯS, рS)≃ А , д, φяS( рS, д) .
может рассматриваться как выполнение интерпретатора I S на аппаратных средств, то есть, как черный ящикготов интерпретировать программынаписанные на языке S .φяS яS S
Функция может рассматриваться как функция, которая специализирует интерпретатор I S для программы P S , как при частичной оценке. Таким образом, число Геделя σ ( я S , р S ) можно видеть , имеет объектный код , который скомпилированная версия программы р S .σ яS пS σ( ЯS, рS) пS
Так что функция можно рассматривать как функцию, которая принимает в качестве аргумента исходный код программы q S, написанный на языке S , и возвращает версию объектного кода для этой программы. Таким образом, C S - это то, что обычно называется компилятор.СS= λ qS, σ( ( ЯS, дS) QS S СS
Некоторые выводы
Формализация более ограничительного представления о том, что такое компилятор, потребует более тонкого теоретического подхода. Я не знаю, что могло быть сделано в этом направлении. Самая реальная работа по частичной оценке более реалистична с инженерной точки зрения. И, конечно же, существуют другие методы написания компиляторов, включая извлечение программ из доказательства их спецификации, разработанные в контексте теории типов, основанные на изоморфизме Карри-Ховарда (но я выхожу за пределы своей компетенции) ,
Моя цель здесь состояла в том, чтобы показать, что замечание Рафаэля не "сумасшедшее", а разумное напоминание о том, что вещи не очевидны и даже не просты. Сказать, что что-то невозможно, является сильным утверждением, которое требует точных определений и доказательств, хотя бы для того, чтобы иметь точное понимание того, как и почему это невозможно . Но построение правильной формализации для выражения такого доказательства может быть довольно трудным.
При этом, даже если конкретная функция не компилируется, в смысле, понятном инженерам, стандартные методы компиляции всегда могут быть применены к частям программ, которые не используют такую функцию, как отмечено в ответе Жиля.
Следуя ключевым замечаниям Жиля, что в зависимости от языка некоторые вещи могут быть выполнены во время компиляции, в то время как другие должны выполняться во время выполнения, что требует специального кода, мы можем видеть, что концепция компиляции на самом деле плохо определен и, вероятно, не может быть определен каким-либо удовлетворительным образом. Компиляция - это всего лишь процесс оптимизации, как я пытался показать в разделе частичной оценки , когда сравнивал его с предварительной обработкой статических данных в некоторых алгоритмах.
Как сложный процесс оптимизации, концепция компиляции фактически принадлежит континууму. В зависимости от характеристик языка или программы, некоторая информация может быть доступна статически и позволяет оптимизировать ее. Другие вещи должны быть отложены на время выполнения. Когда все становится действительно плохо, все должно быть сделано во время выполнения, по крайней мере, для некоторых частей программы, и объединение исходного кода с интерпретатором - это все, что вы можете сделать. Так что это связывание - только нижняя часть этого континуума компиляции. Большая часть исследований компиляторов посвящена поиску способов делать статически то, что раньше делалось динамически. Сборка мусора во время компиляции кажется хорошим примером.
Обратите внимание, что утверждение о том, что процесс компиляции должен генерировать машинный код, не поможет. Это именно то, что может сделать пакетирование, поскольку интерпретатор - это машинный код (ну, с кросс-компиляцией все может быть немного сложнее).
источник
Вопрос не в том, что компиляция невозможна . Если язык можно интерпретировать interpre, то его можно скомпилировать тривиальным способом, связав интерпретатор с исходным кодом. Вопрос в том, какие языковые особенности делают это, по сути, единственным способом.
Интерпретатор - это программа, которая принимает исходный код в качестве входных данных и ведет себя так, как указано в семантике этого исходного кода. Если необходим переводчик, это означает, что в языке есть способ интерпретации исходного кода. Эта функция называется
eval
. Если интерпретатор требуется как часть среды выполнения языка, это означает, что язык включаетeval
: либоeval
существует как примитив, либо он может быть закодирован каким-либо образом. Языки, известные как языки сценариев, обычно включаютeval
функцию, как и большинство диалектов Лисп .То, что язык включает в себя
eval
, не означает, что большая его часть не может быть скомпилирована в нативный код. Например, есть оптимизирующие компиляторы Lisp, которые генерируют хороший нативный код и тем не менее поддерживаютeval
;eval
'Код может быть интерпретирован или может быть скомпилирован на лету.eval
является конечной функцией, требующей интерпретатора, но есть и другие функции, которые требуют чего-то, кроме переводчика. Рассмотрим некоторые типичные фазы компилятора:eval
означает, что все эти этапы должны выполняться во время выполнения. Есть и другие функции, которые затрудняют компиляцию. Взяв это снизу, некоторые языки поощряют поздние ссылки, предоставляя способы, которыми функции (методы, процедуры и т. Д.) И переменные (объекты, ссылки и т. Д.) Могут зависеть от нелокальных изменений кода. Это затрудняет (но не делает невозможным) создание эффективного собственного кода: проще сохранять ссылки на объекты в виде вызовов на виртуальной машине и позволить механизму виртуальной машины обрабатывать привязки на лету.Вообще говоря, рефлексия затрудняет компиляцию языков в нативный код. Примитив eval - крайний случай отражения; многие языки не заходят так далеко, но, тем не менее, имеют семантику, определенную в терминах виртуальной машины, что позволяет, например, коду получать класс по имени, проверять его наследование, перечислять его методы, вызывать метод и т. д. Java с JVM и C # с .NET - два известных примера. Самый простой способ реализовать эти языки - это скомпилировать их в байт-код , но, тем не менее, существуют нативные компиляторы (многие из которых точно вовремя ), которые компилируют по крайней мере фрагменты программы, не использующие расширенные средства отражения.
Проверка типа определяет, является ли программа действительной. Разные языки имеют разные стандарты того, сколько анализа выполняется во время компиляции и во время выполнения: язык известен как «статически типизированный», если он выполняет много проверок перед началом выполнения кода, и «динамически типизированный», если он этого не делает. Некоторые языки включают в себя функцию динамического приведения или функцию «не проверять и проверять»; эти функции требуют встраивания проверки типов в среду выполнения. Это ортогонально требованиям включения генератора кода или интерпретатора в среду выполнения.
¹ Упражнение: определить язык, который нельзя интерпретировать.
источник
eval
просто компилирует байт-код, а затем в качестве отдельного шага бинарный код выполняет байт-код. И это определенно отражается в скомпилированном бинарном файле.int arr[] = {1,2,5};
и генерирует раздел с инициализированными данными, содержащий [1,2,5], но я бы не описал его поведение как перевод [1,2,5] в машинный код. Если почти вся программа должна храниться в виде данных, какая ее часть действительно будет «скомпилирована»?Я думаю, что авторы предполагают, что компиляция означает
Вот некоторые примеры функций, которые сделают его проблематичным, если не «невозможным» для такой схемы:
Если вы можете запросить значение переменной во время выполнения, ссылаясь на переменную по ее имени (которая является строкой), то вам понадобятся имена переменных, чтобы быть рядом во время выполнения.
Если вы можете вызывать функцию / процедуру во время выполнения, ссылаясь на нее по ее имени (которая является строкой), тогда вам понадобятся имена функций / процедур во время выполнения.
Если вы можете создать часть программы во время выполнения (в виде строки), скажем, запустив другую программу или считав ее из сетевого подключения и т. Д., То вам потребуется либо интерпретатор, либо компилятор во время выполнения, чтобы запустить эту часть программы.
Лисп имеет все три функции. Таким образом, системы Lisp всегда имеют интерпретатор, загруженный во время выполнения. Языки Java и C # имеют имена функций, доступные во время выполнения, и таблицы, чтобы посмотреть, что они означают. Вероятно, такие языки, как Basic и Python, также имеют имена переменных во время выполнения. (Я не уверен на 100% в этом).
источник
atexit
обработки. Но это все еще должно быть там.Возможно, что текущие ответы «переосмысливают» утверждение / ответы. возможно, авторы имеют в виду следующее явление. у многих языков есть команда, подобная eval; например, см. javascript eval, и его поведение обычно изучают как особую часть теории CS (например, Lisp). функция этой команды заключается в оценке строки в контексте определения языка. следовательно, в действительности он имеет сходство с «встроенным компилятором». компилятор не может знать содержимое строки до времени выполнения. поэтому компиляция результата eval в машинный код невозможна во время компиляции.
другие ответы указывают на то, что различие интерпретируемых и скомпилированных языков может значительно размываться во многих случаях, особенно в более современных языках, таких как, скажем, Java с «как раз вовремя» компилятором, также называемым «горячей точкой» (движки javascript, например V8, все чаще используют эту же технику). "eval-like" функциональность, безусловно, одна из них.
источник
eval
.LISP - ужасный пример, так как он был задуман как своего рода «машинный» язык более высокого уровня как основа для «настоящего» языка. Сказанный «настоящий» язык никогда не материализовался. Машины LISP были построены на идее выполнения (большей части) LISP в аппаратном обеспечении. Поскольку интерпретатор LISP - это просто программа, в принципе возможно реализовать его в схемотехнике. Возможно, не практично; но далеко не невозможно.
Кроме того, в кремнии запрограммировано много интерпретаторов, обычно называемых «CPU». И часто бывает полезно интерпретировать (пока не существует, нет под рукой ...) машинные коды. Например, Linux 'x86_64 был впервые написан и протестирован на эмуляторах. Когда на рынке появились микросхемы, имелись в наличии полные дистрибутивы, даже для тех, кто только начал тестирование. Java часто компилируется в код JVM, который является интерпретатором, который не составит труда написать в кремнии.
Большинство «интерпретируемых» языков компилируются во внутреннюю форму, которая оптимизируется, а затем интерпретируется. Это, например, то, что делают Perl и Python. Существуют также компиляторы для предполагаемых интерпретируемых языков, например оболочка Unix. С другой стороны, можно интерпретировать традиционно скомпилированные языки. Одним из самых экстремальных примеров, которые я видел, был редактор, который использовал интерпретированный C как язык расширения. Это C может запускать обычные, но простые программы без проблем.
С другой стороны, современные процессоры даже принимают ввод «машинного языка» и переводят его в инструкции более низкого уровня, которые затем переупорядочиваются и оптимизируются (то есть «компилируются») перед передачей для выполнения.
Весь этот «компилятор» против «переводчика» различия действительно спорный вопрос, где - то в стеке есть окончательный интерпретатор , который принимает «код» и выполняет его «напрямую». Ввод от программиста претерпевает преобразования вдоль линии, которая называется «компиляция», просто рисует произвольную линию на песке.
источник
Реальность такова, что существует большая разница между интерпретацией некоторой базовой программы и выполнением ассемблера. И есть промежуточные области с P-кодом / байт-кодом с компиляторами или без них. Поэтому я попытаюсь обобщить некоторые моменты в контексте этой реальности.
Если способ анализа исходного кода зависит от условий выполнения, написание компилятора может оказаться невозможным или настолько сложным, что никто не будет беспокоиться.
Код, который изменяет сам себя, в общем случае невозможно скомпилировать.
Программа, которая использует функцию, подобную eval, обычно не может быть полностью скомпилирована заранее (если вы рассматриваете строку, переданную ей как часть программы), хотя, если вы собираетесь запускать eval'ed код повторно, она все равно может быть полезно, чтобы ваша eval-подобная функция вызывала компилятор. Некоторые языки предоставляют API для компилятора, чтобы упростить это.
Возможность ссылаться на вещи по имени не исключает компиляции, но вам нужны таблицы (как уже упоминалось). Вызов функций по имени (например, IDispatch) требует много усилий, и я думаю, что большинство людей согласятся с тем, что мы фактически говорим о интерпретаторе вызова функций.
Слабая типизация (независимо от вашего определения) делает компиляцию труднее и, возможно, результат менее эффективным, но часто не невозможным, если разные значения не вызывают разный анализ. Здесь есть скользящая шкала: если компилятор не может определить фактический тип, ему нужно будет генерировать ветви, вызовы функций и тому подобное, которые иначе не были бы там, эффективно встраивая биты интерпретатора в исполняемый файл.
источник
я бы предположил, что главной особенностью языка программирования, делающей невозможным компилятор для этого языка (в строгом смысле, см. также самоприемник ), является функция самомодификации . Значение языка позволяет изменять исходный код во время выполнения (компилятор, генерирующий, фиксированный и статический, объектный код не может этого сделать). Классический пример - Лисп (см. Также Гомойконичность ). Аналогичные функциональные возможности предоставляются с использованием языковой конструкции, такой как eval , включенной во многие языки (например, javaScript). Eval фактически вызывает интерпретатор (как функцию) во время выполнения .
Другими словами, язык может представлять свою собственную мета-систему (см. Также Метапрограммирование )
Обратите внимание, что отражение языка в смысле запроса метаданных определенного исходного кода и, возможно, изменения только метаданных (например, механизма отражения Java или PHP) не является проблематичным для компилятора, поскольку он уже имеет те метаданные во время компиляции и могут сделать их доступными для скомпилированной программы при необходимости, если это необходимо.
Еще одна особенность, которая делает компиляцию трудной или не самой лучшей опцией (но не невозможной), - это схема типизации, используемая в языке (т.е. динамическая типизация против статической типизации и строгая типизация против свободной типизации). Это мешает компилятору иметь всю семантику во время компиляции, поэтому эффективно часть компилятора (другими словами, интерпретатор) становится частью сгенерированного кода, который обрабатывает семантику во время выполнения . Другими словами, это не компиляция, а интерпретация .
источник
Я чувствую, что первоначальный вопрос не очень хорошо сформирован. Авторы вопроса, возможно, намеревались задать несколько иной вопрос: какие свойства языка программирования способствуют написанию для него компилятора?
Например, проще написать компилятор для контекстно-свободного языка, чем контекстно-зависимый язык. Грамматика, которая определяет язык, также может иметь проблемы, которые затрудняют его компиляцию, такие как неясности. Такие проблемы могут быть решены, но требуют дополнительных усилий. Точно так же, языки, определяемые неограниченной грамматикой, труднее анализировать, чем контекстно-зависимые языки (см. Иерархию Хомского ). Насколько мне известно, наиболее широко используемые процедурные языки программирования близки к контекстно-свободному, но имеют несколько контекстно-зависимых элементов, что делает их относительно легко компилируемыми.
источник
На вопрос есть правильный ответ, настолько очевидный, что его обычно упускают из виду как тривиальный. Но это имеет значение во многих контекстах и является основной причиной существования интерпретируемых языков:
Компиляция исходного кода в машинный код невозможна, если у вас еще нет исходного кода.
Интерпретаторы добавляют гибкость, и, в частности, они добавляют гибкость запуска кода, который не был доступен при компиляции базового проекта.
источник