Содержание

Особенности интерпретаторов OTUS

Интерпретатор и компилятор – два элемента, которые отвечают за непосредственное преобразование высокоуровневого языка программирования или сценария в машинный код. Несмотря на то, что данные компоненты выполняют одни и те же операции, они отличаются друг от друга.

Далее предстоит разобраться с тем, что собой представляют интерпретаторы и компиляторы. Рассмотрим ключевые особенности каждого «преобразователя» кода, наглядные примеры работы, а также их достоинства и недостатки. Все это пригодится как новичкам, так и опытным разработчикам.

Компиляция

Компилятор – это специальная программа на компьютере. Она переводит имеющийся код с одного языка разработки на другой. Популярный инструмент, без которого трудно представить современное программирование.

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

У интерпретаторов и компиляторов языков есть одна цель – преобразовать исполняемое приложение в машинный код. После выполнения процедуры считывания устройство будет успешно распознавать имеющийся контент. Примеры – это приложения, интерпретация которых проведена через C или C++.

Здесь стоит обратить внимание на следующие моменты:

  1. Компиляторы применяются для программ, которые переводят исходное приложение с высокого уровня на язык разработки более низкого.
  2. Compiler выполняет различные функции. Он может организовывать предварительную обработку данных, семантический анализ, парсинг, а также оптимизацию контента. Это делает работу с приложением более удобным и простым.

Выше – пример того, как выглядит компиляция исходного кода той или иной программы.

Сильные стороны

Компиляторы имеют далеко не одно преимущество. К сильным сторонам соответствующих компонентов относят следующие моменты:

  1. Программный код уже переведен в машинный. На его обработку требуется намного меньше времени.
  2. Документы типа .exe выполняются быстрее, чем исходный код. Объектные программы сохраняются. Это делает приложение более удобным – оно может быть запущено в любое удобное пользователю время.
  3. Полученные объектные приложения сложнее скорректировать. Такие утилиты будут обладать надежной защитой.

А еще программирование с использованием компиляторов предусматривает проверку исходного кода на синтаксические ошибки. Это делает процесс написания софта более быстрым и удобным. Обнаруженная ошибка многими языками будет подчеркиваться. Устранить ее станет намного проще даже новичкам.

Слабые стороны

Несмотря на достоинства, рассматриваемый инструмент имеет недостатки. К ним относят такие моменты:

  1. Использование большого количества памяти на компьютере. Связано это с особенностями выполняемых преобразований.
  2. Затраты по времени. Процесс формирования объектного приложения производится не моментально.
  3. Толкования исходного кода должны быть 100% достоверными и однозначными. В противном случае сформировать объектное программное обеспечение не получится.

Это – только один из двух доступных вариантов преобразования исходного кода. Теперь можно рассмотреть интерпретаторы языков и их особенности.

Интерпретатор

Для преобразования приложений могут использоваться разные инструменты. Программы иногда используют интерпретаторы (interpreters). Так называют специальные компьютерные приложения, которые занимаются преобразованием каждого программного оператора высокого уровня. На выходе получается машинный код.

Сюда включены разные коды: исходные, предварительно скомпилированные, а также разнообразные сценарии.

Интерпретатор языка – машинная программа. Она непосредственно выполняет набор инструкций, а также отвечает за выполнение заданных функций. В ходе операций проводится интерпретация без компилирования. Примеры – языки Python, Matlab, Perl.

Интерпретаторы языков работают так же, как и compilers. Они отвечают за преобразование ЯП высокого уровня в более низкий. А именно – в машинный. Но interpretator выполняет функции при их непосредственном запуске.

Плюсы

Среди основных достоинств интерпретаторов выделяют:

  1. Облегчение работы с исходным кодом.
  2. Использование минимального объема памяти устройства. Связано это с тем, что у интерпретируемых программ используется принцип преобразования по одной инструкции раз за разом.
  3. Вы выполните отладку утилиты намного быстрее и комфортнее. Связано это с тем, что программа-интерпретатор выполняет связку обнаруженного сообщения об ошибке с обрабатываемым контентом.

Такой вариант помогает ускорить исходный исполняемый файл, а также делает работу написанного софта более комфортной на устройствах с небольшим объемом памяти.

Минусы

Интерпретаторы языков кроме преимуществ имеет ряд недостатков. О них должен помнить каждый разработчик.

Интерпретация может затянуть время исполнения программы. Связано это с тем, что каждый раз для запуска нужно поэтапно преобразовывать имеющиеся функции. А еще программы-интерпретаторы выполняются только там, где имеется соответствующий инструментарий. Если на устройстве отсутствует interpreter, воспользоваться приложением не получится.

Как работают инструменты

Стоит обратить внимание на то, как работают рассматриваемые элементы. В случае с компилятором процессы проходят так:

  1. Компилятор создает программу.
  2. Проводится анализ всех операторов языка. На этом этапе сделаем проверку правильности.
  3. При обнаружении ошибок компилятор выдает соответствующее сообщение. В противном случае имеющийся контент переводится в машинный тип.

При компилировании допускается связывание различных кодовых файлов в программы, пригодные для запуска (пример – формат .exe). После этого имеющийся софт успешно запустится.

Интерпретатор работает иначе:

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

Выше – примеры того, как выглядит работа компиляторов и интерпретаторов. Использование этих инструментов обуславливается конкретным языком разработки.

Хотите освоить современную IT-специальность? Огромный выбор курсов по востребованным IT-направлениям есть в Otus!

SoftCraft: функционально-параллельное программирование (интерпретатор)

Функциональная модель параллельных вычислений и язык программирования «Пифагор»


[ <<< | Содержание | Предисловие | Введение | 1 | 2 | 3 |

4 | 5 | Заключение | П1 | П2 | П2 | Источники | >>> ]


© 2002 А. И. Легалов, Ф.А. Казаков, Д.А. Кузьмин, Д.В. Привалихин

Система интерпретации функциональных программ (СИФП) состоит из трех основных модулей:

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

Эти модули реализованы внутри монолитного исполняемого файла. Общая структура СИФП приведена на рис. 4.1.


Рис. 4.1. Структура системы интерпретации функциональных программ.

При получении имени входного файла, содержащего текст функциональной программы, модуль управления проверяет наличие файла с введённым именем и посылает транслятору сигнал о готовности исходного текста программы. Первой задачей транслятора является проверка синтаксиса исходного текста программы и выявление синтаксических ошибок в тексте, допущенных программистом.

Сообщения об этих ошибках передаются модулю управления. Модуль управления выдаёт сообщения об ошибках интерпретации в соответствующее окно сообщений. Вторая задача транслятора — построение промежуточного представления программы на ФЯПП. Промежуточное представление — это отражение структуры и информационных связей между отдельными элементами программы. Обе эти задачи решаются транслятором за один проход исходного текста программы. Если трансляция текста программы была выполнена без ошибок, то промежуточное представление поступает в интерпретатор, который может выполнить любую из оттранслированных функций.

Исходные данные, необходимы для вычислений, вводятся в специальном окне аргументов и анализируются модулем управления. Если интерпретация выполнена без ошибок, в окне результатов модуля управления появляются результаты вычислений. Модуль управления также обеспечивает отладку любой функции, содержащейся в исходном тексте программы, путем ее пошагового выполнения с отображением результатов исполнения каждого шага в отладочном окне. Ниже все модули представлены более подробно.

Необходимо отметить, что транслятор не записывает промежуточного представления в файл, который мог бы послужить исходным материалом для интерпретатора. Это связано с тем, что на данном этапе работ нет смысла хранить и отдельно использовать промежуточное представление. Основной упор был сделан на исследование принципов построения функционально-параллельных программ и возможностей языка.

4.

1. Структура транслятора

Транслятор состоит из лексического и синтаксического анализаторов (рис 4.2).


Рис. 4.2. Обобщенная структура транслятора.

Лексический анализатор (сканер) формирует лексемы и передает их синтаксическому анализатору. Задачей синтаксического анализатора является проверка синтаксиса и построение промежуточного представления программы. При этом синтаксический анализатор производит обращения к интерфейсам уже построенных объектов. Например, при построении объекта <список>, сначала создаётся объект <список>, а затем, по мере построения объектов — элементов списка, производятся обращения к методам уже частично построенного объекта <список> для добавления к нему элементов.

Синтаксический анализатор проверяет принадлежность лексем синтаксическим правилам и формирует промежуточное представление. Под промежуточным представлением понимается одна из внутренних форм представления исходной программы — динамическая модель, с которой интерпретатору удобнее работать, чем с текстовым представлением. Организация промежуточного представления является одной из особенностей разрабатываемой системы. Оно организовано в виде динамических структур данных, отображающих основные конструкции языка: программы, функций, имен, операторов, выражений и пр. Взаимосвязь между различными объектами промежуточного представления приведена на рис. 4.3. Ниже описаны классы реализующие структуру промежуточного представления.

TProgram — класс, описывающий структуру всего исходного файла. Он содержит таблицу имён констант и функций, используемых в программе. Этот класс в данной версии транслятора имеет только одного представителя, т.е. транслятор не позволяет обработку нескольких файлов с текстами программ на ФЯПП. Поле NameOwner этого класса содержит значение NULL, поскольку он является владельцем глобального пространства имён и, кроме того, это позволяет, при поиске идентификатора в таблицах имён, проводить поиск по цепочке владельцев пространств имён, прекращая поиск, как только в поле NameOwner одного из объектов встретится значение NULL. Класс TProgram имеет метод, позволяющий запускать на выполнение или отладку по имени одну из функций из его таблицы имён функций.


Рис. 4.3. Классы, определяющие структуру промежуточного представления.

TFunction — класс, описывающий структуру одной отдельно взятой функции. Он содержит таблицу имен локальных идентификаторов и указатель на неименованное выражение, задающее информационный граф функции. В качестве владельца пространства имён, в котором находится та или иная функция, всегда выступает представитель класса TProgram, поскольку функция не может быть описана в теле другой функции.

TExpression — класс, представляющий список термов, приведённый к постфиксной форме записи. Каждый из термов может иметь указатель на другой терм, что отражает альтернативную ветвь вычислений (так называемый else-терм).

TTerm — базовый класс, порождающий все классы, выступающие в качестве термов: TBlock, TAtom, TKW, TList, TID.

TBlock — класс, реализующий блок. Аналогичен классу TFunction, за исключением того, что владельцем его пространства имён могут выступать только экземпляры классов TFunction и TBlock, поскольку блок не может быть описан вне тела функции или блока.

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

TKW — класс, представляющий ключевые слова, список которых приведён в кратком описании языка. Хранимое значение равно значению соответствующей лексемы — ключевого слова.

TList — класс, обеспечивающий представление всех трех разновидности списков языка: последовательные списки, параллельные списки и задержанные списки. Содержит список объектов, каждый из которых является представителем класса TExpression.

TID — класс, являющийся отражением идентификаторов в промежуточном представлении. Содержит поле с именем этого идентификатора.

Каждый из классов имеет поле данных, в котором хранится указатель на объект — владелец пространства имён, в котором находится представитель данного класса. Это позволяет использовать идентификаторы, содержащиеся в таблице имен функции, внутри блока, описанного в теле функции. В качестве объектов — владельцев пространства имён могут выступать только представители классов, содержащих таблицу имен.

Любой из объектов, имеющих таблицу имён, содержит список таблиц для значений локальных именованных выражений. Этот список очищается по окончании трансляции. Каждый вызов функции во время интерпретации добавляет к нему одну таблицу имен. Завершение функции ведет к очистке ее таблицы. Это позволяет описывать рекурсивные вызовы функции без возникновения конфликтов между наборами данных формируемых при различных вызовах одной функции.

4.2 Структура интерпретатора

Задача интерпретатора — провести разметку информационного графа программы, написанной на ФЯПП, в соответствии с алгеброй преобразований, аксиомами языка и правилами интерпретации. Каждому ребру информационного графа ставится в соответствие специальная структура — «фишка», отражающая обрабатываемые данные.

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

  1. класса TAtomFishka, предназначенного для хранения скалярных значений всех типов;
  2. класса TListFishka, хранящего элементы классов последовательных, параллельных и задержанных списков, производных от Tfishka;
  3. класса TObjectFishka, содержащего указатель на один из объектов, составляющих промежуточное представление программы, и служащий для разметки дуг информационного графа функциональными фишками.

Всех представителей описанных выше классов далее будем называть «фишками». Именно фишки и будут размещены на рёбрах графа в качестве его разметки.

Каждый из объектов промежуточного представления, при необходимости его интерпретации, получает запрос на вычисление, в структуру которого входят следующие элементы:

  1. Фишка — аргумент функции, в теле которой происходят текущие вычисления. Запрос объекту TProgram в качестве аргумента получает фишку — результат интерпретации введённого пользователем аргумента.
  2. Булевская переменная, принимающая значение false при выполнении программы и false — при её пошаговой отладке. Это значение используется в процессе интерпретации выражений и сигнализирует ему о необходимости делать задержку после каждой операции интерпретации и выдавать на консоль данные, полученные при выполнении данной операции.

Результат вычислений каждого из объектов состоит из двух элементов:

  1. Фишка — результат вычислений (в том числе и фишка ошибки).
  2. Булевская переменная, значение TRUE которой является признаком того, что фишка-результат содержит в себе фишки ошибок либо сама является таковой. Это позволяет, не производя анализа полученной фишки определить успешность вычислений.
4.2.1 Интерпретация объектов TProgram, TFunction и TBlock

Для выполнения той или иной функции модуль управления посылает запрос объекту класса TProgram, который производит поиск имени в таблице идентификаторов и переадресует запрос функции, указатель на которую хранится в таблице (рис 4.4). Результатом вычисления функции является фишка, полученная после интерпретации его неименованного выражения. Результатом вычисления объекта класса TBlock также является фишка, полученная после интерпретации его неименованного выражения.


Рис. 4.4. Выполнение запроса на вычисление функции.

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

4.2.2 Интерпретация объекта TExpression

Объект класса TExpression представляет собой список термов выражения, приведённого к постфиксной форме:


term1:term2:term3:term4: . .. :termn-1:termn.


Каждый из термов может быть атомом, ключевым словом, списком, блоком или идентификатором. Если рассматриваемое выражение является неименованным выражением какой либо функции или блока, то termnдолжен быть ключевым словом: return — в случае функции и break — в случае блока. Term1 не может быть идентификатором функции, поскольку слева от знака операции интерпретации должен стоять аргумент функции даже в том случае, если функция не использует аргумента.

Выполнение выражения начинается с вычисления первого терма списка (рис 4.5). Если вычисление не вызвало фатальных ошибок интерпретации, то вычисляется второй терм списка. Если и в этом случае всё прошло без ошибок, то фишки, полученные в качестве результата, передаются на два входа блока операции интерпретации, который реализован в виде функции, возвращающей значение True при безошибочной интерпретации и False — в противном случае. Фишка, выдаваемая блоком, является результатом операции интерпретации, где аргументом выступает фишка — результат вычисления первого терма списка, а функцией — результат вычисления второго.


Рис. 4.5. Вычисление выражения

Если результатом операции интерпретации стала фишка ошибки или список, содержащий фишку ошибки на некотором уровне вложенности, то интерпретатор определяет наличие у второго терма указателя на else-терм. В случае его наличия результатом данной операции интерпретации станет фишка, полученная в результате интерпретации этого терма. Если else-терм отсутствует, то фишка ошибки продолжает продвижение по информационному графу в качестве разметки его дуг, и дальнейшие операции интерпретации формируют также фишку ошибки.

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

4.2.3 Интерпретация объектов TAtom и TKW

При интерпретации атомов и ключевых слов в поле хранимого значения порождаемой фишки записывается значение, находящееся в соответствующем поле объектов атом или ключевое слово (рис 4.6).

4.2.4 Интерпретация объекта TList

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

Исключение составляет задержанный список. При его интерпретации выражения — элементы списка не подвергаются вычислению. Создаются объекты класса TObjectFishka, в поле Object которых записываются указатели на элементы исходного списка. Эти указатели будут использованы в дальнейшем при раскрытии задержанного списка. Полученные объекты становятся элементами результирующей фишки — списка фишек.


Рис. 4.6. Интерпретация атомов и ключевых влов.


Рис. 4.7. Интерпретация списков.

В случае, когда исходный задержанный список содержит только один элемент, результатом интерпретации такого списка станет только одна фишка типа TObjectFishka, содержащая указатель на единственный элемент исходного списка.

4.2.5 Интерпретация объекта TID

Интерпретация идентификатора, являющегося одним из термов выражения, представляет собой интерпретацию объекта, именем-ярлыком которого является рассматриваемый идентификатор. При этом возникает задача поиска идентификатора в таблицах локальных значений выражений, в таблицах локальных идентификаторов, в таблице функций и таблице констант программы, а также определение типа объекта, скрывающегося за этим идентификатором. Последовательность действий при этом такова (рис. 4.8):

  1. Поиск имени в поле идентификатора параметра функции, содержащей выражение, в котором встретился идентификатор. Так как данный пункт является первым, то отсюда вытекает, что невозможно в теле функции переопределить идентификатор параметра, поименовав им какое-либо выражение.

Если идентификатор найден, то фишка, передаваемая в качестве одного из элементов запроса на вычисление функции, и соответствующая аргументу функции, станет результатом интерпретации данного идентификатора.

  1. Поиск идентификатора в таблице имён функций. Для этого по цепочке указателей на владельцев пространств имён происходит подъём по дереву объектов до объекта, не имеющего владельца, т. е. до объекта класса TProgram. В его таблице имён идентификаторов происходит поиск рассматриваемого имени.

В случае успешного поиска создаётся фишка класса TObjectFishka, в поле хранимого значения которого записывается указатель на объект — функцию, используемый затем в блоке операции интерпретации.

  1. Поиск идентификатора в таблицах значений именованных выражений и таблицах локальных идентификаторов. Если интерпретируемый идентификатор встретился в таблице значений именованных выражений, это значит, что выражение, именованное этим идентификатором, уже вычислено и соответствующая фишка из таблицы возвращается в качестве результата.


Рис. 4.8. Интерпретация элемента таблицы имен.

Если идентификатор не найден в таблице значений, происходит поиск в таблице имён. При успехе поиска результатом интерпретации идентификатора становится фишка — результат вычисления выражения, указатель на которое находится в таблице. Если выражение вычислено, то полученное значение заносится в таблицу значений именованных выражений для дальнейшего использования.

Если идентификатор не найден ни в одной из таблиц, происходит запрос на интерпретацию идентификатора к владельцу пространства имён, в котором находится рассматриваемое неименованное выражение. Каждый из объектов, получивший такой запрос, производит все действия из пункта 3, за исключением объекта TProgram, который производит поиск идентификатора в таблице констант.

4.2.6 Оператор интерпретации

Оператор интерпретации соответствует одноимённому программо-формирующему оператору информационного графа. Он осуществляет все действия, предусматриваемые правилами функционирования модели вычислений.

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

Перед выполнением операции интерпретации, над обеими входными фишками, в случае необходимости, выполняются действия в соответствии с правилами эквивалентных преобразований и правил интерпретации списков, что в ряде случаев приводит к рекурсивным вызовам функции, осуществляющей операцию интерпретации. Затем выполняется операция интерпретации для предопределённых операций, описанных ниже.

В случае, когда в качестве функции выступает функциональная фишка, производится запрос на вычисление к объекту, указатель на который она содержит. При этом в качестве аргумента запроса передаётся фишка, интерпретируемая данным оператором как аргумент.

4.2.7 Правила интерпретации предопределенных операций

Интерпретация предопределенных операций задается правилами их выполнения заданными в описании ФЯПП (раздел 2.15).

4.3. Модуль управления

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

  1. текстовый редактор;
  2. вспомогательные утилиты, облегчающие просмотр отдельных функций;
  3. окна отладки, аргументов, результатов.
  4. Инструментальные панели и меню, позволяющие управлять доступом к транслятору, интерпретатору промежуточного представления и прочим функциям.

Более подробное описание модуля управления приведено в разделе 5.

4.4 Использованные средства разработки

Система реализована на языке С++ при использовании компилятора и среды разработки Microsoft Visual C++ 6.0. Для создания объектов-контейнеров была использована стандартная библиотека шаблонов STL (Standard Templates Library). При написании интерактивной среды разработки программ под Windows были также использованы классы библиотеки MFC (Microsoft Foundation Library).

Примечание. Сведения представлены по сосотоянию на январь 2002 г.


[ <<< | Содержание | Предисловие | Введение | 1 | 2 | 3 | 4 | 5 | Заключение | П1 | П2 | П2 | Источники | >>> ]

Написание интерпретатора с нуля

Некоторые говорят, что «все сводится к единицам и нулям» — но действительно ли мы понимаем, как наши программы преобразуются в эти биты?

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

Что нужно и что нельзя делать при написании собственного интерпретатора

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

Правильное написание синтаксических анализаторов вручную может быть сложной задачей со всеми задействованными пограничными случаями. Вот почему существуют популярные инструменты, такие как ANTLR, которые могут генерировать синтаксические анализаторы для многих популярных языков программирования. Существуют также библиотеки, называемые комбинаторами синтаксических анализаторов , которые позволяют разработчикам писать синтаксические анализаторы непосредственно на предпочитаемых ими языках программирования. Примеры включают FastParse для Scala и Parsec для Python.

Мы рекомендуем читателям в профессиональном контексте использовать такие инструменты и библиотеки, чтобы не изобретать велосипед. Тем не менее, понимание проблем и возможностей написания интерпретатора с нуля поможет разработчикам более эффективно использовать такие решения.

Обзор компонентов интерпретатора

Интерпретатор — это сложная программа, поэтому она состоит из нескольких этапов:

  1. лексер — это часть интерпретатора, которая преобразует последовательность символов (обычный текст) в последовательность символов. жетоны.
  2. Анализатор , в свою очередь, берет последовательность токенов и создает абстрактное синтаксическое дерево (AST) языка. Правила, по которым работает синтаксический анализатор, обычно определяются формальной грамматикой.
  3. Интерпретатор — это программа, которая интерпретирует AST исходного кода программы на лету (без предварительной компиляции).

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

 val input="2*7+5"
токены val = Lexer(input).lex()
val ast = Parser(токены).parse()
val res = Интерпретатор(ast).interpret()
println(s"Результат: $res")
 

После трех этапов мы ожидаем, что этот код вычислит окончательное значение и напечатает Результат: 19 . В этом руководстве используется Scala, потому что он:

  • Очень лаконичный, умещает большой объем кода на одном экране.
  • Ориентирован на выражения, без необходимости использования неинициализированных/нулевых переменных.
  • Надежный тип, с мощной библиотекой коллекций, перечислениями и классами case.

В частности, код здесь написан в синтаксисе необязательных фигурных скобок Scala3 (подобный Python синтаксис на основе отступов). Но ни один из подходов не является специфичным для Scala , а Scala похожа на многие другие языки: читатели найдут простым преобразование этих примеров кода в другие языки. За исключением этого, примеры можно запускать онлайн с помощью Scastie.

Наконец, секции Lexer, Parser и Interpreter содержат различных примера грамматик . Как показано в соответствующем репозитории GitHub, зависимости в более поздних примерах немного меняются для реализации этих грамматик, но общие концепции остаются прежними.

Компонент интерпретатора 1: Написание лексера

Допустим, мы хотим лексировать эту строку: "123 + 45 true * false1" . Он содержит различные типы токенов:

  • Целочисленные литералы
  • А + оператор
  • А * оператор
  • A истинный буквальный
  • Идентификатор, false1

В этом примере пробелы между токенами будут пропущены.

На данном этапе выражения не обязательно должны иметь смысл; лексер просто преобразует входную строку в список токенов. (Работа по «осмыслению токенов» возложена на синтаксический анализатор.)

Мы будем использовать этот код для представления токена:

 case class Token(
  tpe: Token.Type,
  текст: строка,
  startPos: Int
)
Токен объекта:
  Тип перечисления:
    случай Число
    чехол Плюс
    чехол раз
    Идентификатор случая
    случай Истинно
    случай Ложь
    случай EOF
 

Каждый токен имеет тип, текстовое представление и позицию в исходном вводе. Позиция может помочь конечным пользователям лексера с отладкой.

Маркер EOF — это специальный маркер, который отмечает конец ввода. Его нет в исходном тексте; мы используем его только для упрощения этапа парсера.

Это будет вывод нашего лексера:

 Ввод лексинга:
123 + 45 правда * ложь1
Токены:
Список(
  Токен (tpe = число, текст = "123", tokenStartPos = 0),
  Токен (tpe = Плюс, текст = "+", tokenStartPos = 4),
  Токен (tpe = число, текст = "45", tokenStartPos = 6),
  Token(tpe = True, text = "true", tokenStartPos = 9),
  Токен (tpe = раз, текст = "*", tokenStartPos = 14),
  Токен (tpe = идентификатор, текст = "false1", tokenStartPos = 16),
  Token(tpe = EOF, text = "", tokenStartPos = 22)
)
 

Давайте рассмотрим реализацию:

 class Lexer(input: String):
  def lex(): Список[Токен] =
    val tokens = mutable.ArrayBuffer.empty[Token]
    переменная текущая позиция = 0
    в то время как currentPos < input.length делать
      val tokenStartPos = currentPos
      val lookahead = input (currentPos)
      если lookahead.isWhitespace то
        currentPos += 1 // игнорировать пробелы
      иначе, если смотреть вперед == '+' тогда
        текущийПос += 1
        tokens += Token(Type. Plus, lookahead.toString, tokenStartPos)
      иначе, если смотреть вперед == '*' тогда
        текущийПос += 1
        tokens += Token(Type.Times, lookahead.toString, tokenStartPos)
      иначе если lookahead.isDigit тогда
        переменный текст = ""
        в то время как currentPos < input.length && input(currentPos).isDigit do
          текст += ввод (currentPos)
          текущийПос += 1
        tokens += Token(Type.Num, text, tokenStartPos)
      else if lookahead.isLetter then // сначала должна быть буква
        переменный текст = ""
        в то время как currentPos < input.length && input(currentPos).isLetterOrDigit do
          текст += ввод (currentPos)
          текущийПос += 1
        val tpe = совпадение текста
          case "true" => Type.True // специальные регистровые литералы
          case "false" => Type.False
          case _ => Type.Identifier
        tokens += Token(tpe, text, tokenStartPos)
      еще
        error(s"Неизвестный символ $lookahead в позиции $currentPos")
    tokens += Token(Type. EOF, "", currentPos) // специальный маркер конца
    tokens.toList
 

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

Мы используем упреждающий символ, чтобы определить тип следующего токена . Обратите внимание, что опережающий символ не всегда является самым дальним исследуемым символом. Основываясь на предварительном просмотре, мы знаем, как выглядит токен, и используем currentPos для сканирования всех ожидаемых символов в текущем токене, а затем добавляем токен в список:

Если опережение содержит пробелы, мы пропускаем его. Однобуквенные токены тривиальны; мы добавляем их и увеличиваем индекс. Для целых чисел нам нужно позаботиться только об индексе.

Теперь мы подошли к кое-чему немного сложному: идентификаторы против литералов. Правило состоит в том, что мы берем максимально длинное совпадение и проверяем, является ли оно литералом; если нет, то это идентификатор.

Будьте осторожны при работе с такими операторами, как < и <= . Там вы должны посмотреть вперед еще один символ и посмотреть, если это = , прежде чем сделать вывод, что это оператор <= . В противном случае это просто < .

После этого наш лексер создал список токенов.

Компонент интерпретатора 2: Написание синтаксического анализатора

Мы должны дать некоторую структуру нашим токенам — мы мало что можем сделать со списком. Например, нам нужно знать:

Какие выражения являются вложенными? Какие операторы применяются в каком порядке? Какие правила области применения применяются, если таковые имеются?

Древовидная структура поддерживает вложенность и порядок. Но сначала мы должны определить некоторые правила построения деревьев. Мы хотели бы, чтобы наш синтаксический анализатор был однозначным — всегда возвращал одну и ту же структуру для данного ввода.

Обратите внимание, что следующий синтаксический анализатор не использует предыдущий пример лексера . Это для добавления чисел, поэтому его грамматика имеет только две лексемы, '+' и NUM :

 expr -> expr '+' expr
выражение -> ЧИСЛО
 

Эквивалент с использованием вертикальной черты ( | ) как символ «или», как и в регулярных выражениях:

 expr -> expr '+' expr | ЧИСЛО
 

В любом случае у нас есть два правила: одно говорит, что мы можем суммировать два expr s, а другое говорит, что expr может быть токеном NUM , что здесь будет означать неотрицательное целое число.

Правила обычно задаются формальной грамматикой . Формальная грамматика состоит из: Сами правила, как показано выше Начальное правило (первое указанное правило согласно соглашению) Два типа символов для определения правил: Терминалы: «буквы» (и другие символы) нашего языка — несократимые символы, из которых состоят токены. Нетерминалы: промежуточные конструкции, используемые для синтаксического анализа (т. е. символы, которые можно заменить)

Слева от правила может находиться только нетерминал; правая часть может иметь как терминалы, так и нетерминалы. В приведенном выше примере терминалами являются '+' и NUM , а единственным нетерминалом является expr . Для более широкого примера, в языке Java у нас есть терминалы, такие как 'true' , '+' , Identifier и '[' , и нетерминалы, такие как BlockStatements , ClassBody 4, и

2 MethodOrFieldDecl .

Есть много способов реализовать этот синтаксический анализатор. Здесь мы будем использовать метод разбора «рекурсивный спуск». Это самый распространенный тип, потому что его проще всего понять и реализовать.

Анализатор рекурсивного спуска использует одну функцию для каждого нетерминала в грамматике. Он начинается с начального правила и спускается оттуда (отсюда «спуск»), выясняя, какое правило применить в каждой функции. «Рекурсивная» часть жизненно важна, потому что мы можем рекурсивно вкладывать нетерминалы! Регулярные выражения не могут этого сделать: они даже не могут обрабатывать сбалансированные скобки. Поэтому нам нужен более мощный инструмент.

Парсер для первого правила будет выглядеть примерно так (полный код):

 def expr() =
  выражение()
  есть('+')
  выражение()
 

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

Неоднозначность грамматики

Первая проблема — неоднозначность нашей грамматики, которая может быть незаметна на первый взгляд:

 выражение -> выражение '+' выражение | ЧИСЛО
 

Учитывая ввод 1 + 2 + 3 , наш синтаксический анализатор может сначала вычислить либо левое выражение , либо правое выражение в результирующем AST:

Левосторонние и правосторонние AST.

Вот почему нам нужно ввести некоторую асимметрию :

 expr -> expr '+' NUM | ЧИСЛО
 

Набор выражений, которые мы можем представить с помощью этой грамматики, не изменился со времени ее первой версии. Только сейчас однозначно : Анализатор всегда идет влево. Как раз то, что нам было нужно!

Это делает нашу операцию + левой ассоциативной , но это станет очевидным, когда мы перейдем к разделу Интерпретатор.

Лево-рекурсивные правила

К сожалению, приведенное выше исправление не решает другую нашу проблему, левую рекурсию:

 def expr() =
  выражение()
  есть('+')
  есть(ЧИСЛО)
 

У нас есть бесконечная рекурсия здесь. Если бы мы вошли в эту функцию, то в конечном итоге получили бы ошибку переполнения стека. Но теория разбора может помочь!

Предположим, у нас есть такая грамматика, где альфа может быть любой последовательностью терминалов и нетерминалов:

 A -> A альфа | Б
 

Мы можем переписать эту грамматику как:

 A -> B A'
А' -> альфа А' | эпсилон
 

Здесь эпсилон — пустая строка — ничего, нет токена.

Возьмем текущую версию нашей грамматики:

 expr -> expr '+' NUM | ЧИСЛО
 

Следуя описанному выше методу перезаписи правил синтаксического анализа с alpha является нашим '+' токеном NUM , наша грамматика становится:

 expr -> NUM exprOpt
exprOpt -> '+' ЧИСЛО exprOpt | эпсилон
 

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

 class Parser(allTokens: List[Token]):
  импортировать Token.Type
  
  частные токены var = allTokens
  частный var lookahead = tokens.head
  
  деф синтаксический анализ(): Единица измерения =
    выражение()
    если lookahead.tpe != Type.EOF, то
      error(s"Неизвестный токен '${lookahead.text}' в позиции ${lookahead.tokenStartPos}")
  частное выражение выражения(): Unit =
    есть(Тип.Число)
    exprOpt()
  
  частная защита exprOpt(): Unit =
    если lookahead. tpe == Type.Plus, то
      есть(Тип.Плюс)
      есть(Тип.Число)
      exprOpt()
    // иначе: конец рекурсии, эпсилон
  
  частное определение (tpe: Type): Unit =
    если lookahead.tpe != tpe, то
      error(s"Ожидается: $tpe, получено: ${lookahead.tpe} в позиции ${lookahead.startPos}")
    жетоны = жетоны.хвост
    просмотр вперед = tokens.head
 

Здесь мы используем токен EOF , чтобы упростить наш синтаксический анализатор. Мы всегда уверены, что в нашем списке есть хотя бы один токен, поэтому нам не нужно обрабатывать частный случай пустого списка.

Кроме того, если мы переключимся на потоковый лексер, у нас будет не список в памяти, а итератор, поэтому нам нужен маркер, чтобы знать, когда мы подошли к концу ввода. Когда мы подойдем к концу, токен EOF должен быть последним оставшимся токеном.

Просматривая код, мы видим, что выражение может быть просто числом. Если ничего не осталось, следующий жетон не будет Плюс , чтобы мы прекратили парсинг. Последним токеном будет EOF , и мы закончим.

Если во входной строке больше токенов, то они должны выглядеть как + 123 . Вот где рекурсия по exprOpt() срабатывает!

Генерация AST

Теперь, когда мы успешно проанализировали наше выражение, трудно что-либо сделать с ним как есть. Мы могли бы поместить несколько обратных вызовов в наш парсер, но это было бы очень громоздко и нечитаемо. Вместо этого мы вернем AST, дерево, представляющее входное выражение:

 класс случая Expr(num: Int, exprOpt: ExprOpt)
перечисление Expropt:
  case Opt(num: Int, exprOpt: ExprOpt)
  чехол Эпсилон
 

Это похоже на наши правила, использующие простые классы данных.

Теперь наш синтаксический анализатор возвращает полезную структуру данных:

 class Parser(allTokens: List[Token]):
  импортировать Token.Type
  
  частные токены var = allTokens
  частный var lookahead = tokens.head
  
  деф разбор(): Выражение =
    val res = expr()
    если lookahead. tpe != Type.EOF, то
      error(s"Неизвестный токен '${lookahead.text}' в позиции ${lookahead.tokenStartPos}")
    еще
      разрешение
  частное выражение выражения(): выражение =
    val num = есть(Тип.Число)
    Expr(num.text.toInt, exprOpt())
  
  частная защита exprOpt(): ExprOpt =
    если lookahead.tpe == Type.Plus, то
      есть(Тип.Плюс)
      val num = есть(Тип.Число)
      ExprOpt.Opt(num.text.toInt, exprOpt())
    еще
      Экспроопт.Эпсилон
 

Информацию о eat() , error() и других деталях реализации см. в соответствующем репозитории GitHub.

Упрощение правил

Наш нетерминал ExpOpt можно улучшить:

 '+' NUM expOpt | эпсилон
 

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

 ('+' NUM)*
 

Эта конструкция просто означает '+' NUM встречается ноль или более раз.

Теперь наша полная грамматика выглядит так:

 expr -> NUM exprOpt*
exprOpt -> '+' ЧИСЛО
 

И наш AST выглядит лучше:

 case class Expr(num: Int, exprOpts: Seq[ExprOpt])
класс case ExprOpt (число: Int)
 

Полученный синтаксический анализатор такой же длины, но более простой для понимания и использования. Мы исключили Epsilon , что теперь подразумевается, если начать с пустой структуры.

Нам даже 9 не понадобилось0041 ExprOpt класс здесь. Мы могли бы просто указать case class Expr(num: Int, exprOpts: Seq[Int]) или в формате грамматики NUM ('+' NUM)* . Так почему же мы этого не сделали?

Учтите, что если бы у нас было несколько возможных операторов, таких как - или * , то у нас была бы такая грамматика:

 expr -> NUM exprOpt*
exprOpt -> [+-*] ЧИСЛО
 

В этом случае AST требуется ExpOpt для размещения типа оператора:

 case class Expr(num: Int, exprOpts: Seq[ExprOpt])
класс case ExprOpt (op: String, num: Int)
 

Обратите внимание, что синтаксис [+-*] в грамматике означает то же самое, что и в регулярных выражениях: «один из этих трех символов». Мы скоро увидим это в действии.

Компонент интерпретатора 3: Написание интерпретатора

Наш интерпретатор будет использовать наш лексер и синтаксический анализатор, чтобы получить AST нашего входного выражения, а затем оценить это AST любым удобным для нас способом. В данном случае мы имеем дело с числами и хотим вычислить их сумму.

В реализации нашего примера интерпретатора мы будем использовать эту простую грамматику:

 expr -> NUM exprOpt*
exprOpt -> [+-] ЧИСЛО
 

И этот AST:

 case class Expr(num: Int, exprOpts: Seq[ExprOpt])
класс case ExprOpt (op: Token.Type, num: Int)
 

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

Теперь посмотрим, как написать интерпретатор для приведенной выше грамматики:

 class Interpreter(ast: Expr):
  деф интерпретировать (): Int = eval (аст)
  частная оценка (выражение: выражение): Int =
    var tmp = expr. num
    expr.exprOpts.foreach { exprOpt =>
      если exprOpt.op == Token.Type.Plus
      затем tmp += exprOpt.num
      иначе tmp -= exprOpt.num
    }
    температура
 

Если мы разобрали наши входные данные в AST без ошибок, мы уверены, что у нас всегда будет хотя бы одна ЧИСЛО . Затем мы берем необязательные числа и добавляем их к нашему результату (или вычитаем из него).

Замечание с самого начала о левой ассоциативности + теперь ясно: мы начинаем с крайнего левого числа и добавляем другие, слева направо. Это может показаться неважным для сложения, но рассмотрим вычитание: выражение 5 - 2 - 1 оценивается как (5 - 2) - 1 = 3 - 1 = 2 , а не как 5 - (2 - 1) = 5 - 1 = 4 !

Но если мы хотим выйти за рамки интерпретации операторов плюс и минус, нужно определить еще одно правило.

Приоритет

Мы знаем, как анализировать простое выражение, такое как 1 + 2 + 3 , но когда дело доходит до 2 + 3 * 4 + 5 , у нас возникает небольшая проблема.

Большинство людей согласны с тем, что умножение имеет более высокий приоритет, чем сложение. Но парсер этого не знает. Мы не можем просто вычислить его как ((2 + 3) * 4) + 5 . Вместо этого нам нужно (2 + (3 * 4)) + 5 .

Это означает, что нам нужно сначала оценить умножение . Умножение должно быть на дальше от корня AST , чтобы принудительно оценить его перед добавлением. Для этого нам нужно ввести еще один уровень косвенности.

Исправление наивной грамматики от начала до конца

Это наша исходная леворекурсивная грамматика, не имеющая правил приоритета:

 expr -> expr '+' expr | выражение '*' выражение | ЧИСЛО
 

Во-первых, мы даем ему правила приоритета и удаляем его неоднозначность :

 выражение -> выражение '+' термин | срок
термин -> термин '*' ЧИСЛО | ЧИСЛО
 

Затем он получает нелеворекурсивных правила :

 expr -> term exprOpt*
exprOpt -> '+' термин
срок -> ЧИСЛО срокОпт*
termOpt -> '*' ЧИСЛО
 

Результатом является красиво выразительный AST:

 case class Expr(term: Term, exprOpts: Seq[ExprOpt])
класс случая ExprOpt(term: Term)
класс case Term (число: Int, termOpts: Seq[TermOpt])
класс case TermOpt (число: Int)
 

Это дает нам краткую реализацию интерпретатора:

 class Interpreter(ast: Expr):
  деф интерпретировать (): Int = eval (аст)
  частная оценка (выражение: выражение): Int =
    var tmp = eval(expr. term)
    expr.exprOpts.foreach { exprOpt =>
      tmp += eval(exprOpt.term)
    }
    температура
  частная оценка (срок: срок): Int =
    var tmp = термин.номер
    term.termOpts.foreach {termOpt =>
      tmp *= termOpt.num
    }
    температура
 

Как и прежде, идеи в отношении необходимого лексера и грамматики были рассмотрены ранее, но при необходимости читатели могут найти их в репозитории.

Следующие шаги в написании интерпретаторов

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

В наших примерах лексеров, синтаксических анализаторов и интерпретаторов мы только поверхностно коснулись теорий, лежащих в основе компиляторов и интерпретаторов, которые охватывают такие темы, как:

  • Области действия и таблицы символов
  • Статические типы
  • Оптимизация времени компиляции
  • Статические анализаторы программ и линтеры
  • Форматирование кода и красивая печать
  • Доменные языки

Для дальнейшего чтения я рекомендую следующие ресурсы:

  • Шаблоны языковой реализации Теренса Парра
  • Бесплатная онлайн-книга, Crafting Interpreters , Боба Нистрома
  • Введение в грамматику и синтаксический анализ Пола Клинта
  • Написание хороших сообщений об ошибках компилятора Калеб Мередит
  • Заметки из курса Университета Восточной Каролины «Перевод и компиляция программ»

Понимание основ

  • Как создать интерпретатор?

    Чтобы сначала создать интерпретатор, вам нужно создать лексер для получения токенов вашей программы ввода. Затем вы создаете синтаксический анализатор, который берет эти токены и, следуя правилам формальной грамматики, возвращает AST вашей входной программы. Наконец, интерпретатор берет этот AST и каким-то образом интерпретирует его.

  • В чем разница между компилятором и интерпретатором?

    Компилятор берет программу на языке более высокого уровня и преобразует ее в программу на языке более низкого уровня. Интерпретатор берет программу и запускает ее на лету. Он не создает никаких файлов.

  • На каком языке написан переводчик?

    Интерпретаторы могут быть написаны на любом языке программирования. Популярным выбором являются функциональные языки, потому что они имеют отличные абстракции для преобразования данных.

  • Как работает переводчик?

    Интерпретаторы в основном выполняют по одному оператору за раз. Они берут AST, возвращенный синтаксическим анализатором, и выполняют его. В этом процессе они обычно используют некоторые вспомогательные конструкции, такие как таблицы символов, генераторы и оптимизаторы.

  • Как работает лексер?

    Лексер принимает строку символов и возвращает список токенов, которые в основном представляют собой сгруппированные символы. Токены обычно определяются с помощью регулярных выражений.

  • Как работают синтаксические анализаторы программирования?

    Парсер программирования определяется формальной грамматикой, описывающей правила языка, который он анализирует. Наиболее распространенным видом является анализатор рекурсивного спуска, и он напоминает данную грамматику, имея одну функцию для каждого нетерминала. Он принимает последовательность токенов в качестве входных данных и возвращает AST в качестве выходных данных.

  • Что подразумевается под абстрактным синтаксическим деревом?

    Абстрактное синтаксическое дерево (AST) представляет собой представление структуры исходного кода программы. Он содержит только те данные, которые важны для интерпретатора или компилятора. Он не содержит пробелов, фигурных скобок, точек с запятой и подобных частей входной программы.

  • Для чего используется абстрактное синтаксическое дерево?

    Абстрактное синтаксическое дерево используется как промежуточное представление входной программы. Затем интерпретатор/компилятор может делать с ним все, что ему нужно: оптимизировать, упрощать, выполнять или что-то еще.

Кто такой переводчик? - Определение из Techopedia

Что означает переводчик?

Интерпретатор — это компьютерная программа, которая используется для непосредственного выполнения программных инструкций, написанных с использованием одного из многих языков программирования высокого уровня.

Advertisements

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

Интерпретатор Techopedia объясняет

Люди могут понимать только языки высокого уровня, которые называются исходным кодом. Компьютеры, с другой стороны, могут понимать только программы, написанные на двоичных языках, поэтому требуется либо интерпретатор, либо компилятор.

Языки программирования реализуются двумя способами: интерпретацией и компиляцией. Как следует из названия, интерпретатор преобразует или интерпретирует программный код высокого уровня в код, понятный машине (машинный код), или в промежуточный язык, который также может быть легко выполнен.

Интерпретатор читает каждый оператор кода, а затем преобразует или выполняет его напрямую. Напротив, ассемблер или компилятор преобразует исходный код высокого уровня в собственный (скомпилированный) код, который может выполняться непосредственно операционной системой (например, путем создания программы .exe).

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

В большинстве случаев компилятор предпочтительнее, так как его вывод выполняется намного быстрее, чем построчная интерпретация. Вместо того, чтобы сканировать всю программу и переводить ее в машинный код, как это делает компилятор, интерпретатор переводит код по одному оператору за раз.

Хотя время анализа исходного кода сокращается, особенно особенно большого, время выполнения для интерпретатора сравнительно медленнее, чем для компилятора. Вдобавок ко всему, поскольку интерпретация выполняется для каждой строки или оператора, ее можно остановить в середине выполнения, чтобы разрешить либо модификацию кода, либо отладку.

Компиляторы должны генерировать промежуточный объектный код, который требует больше памяти для компоновки, в отличие от интерпретаторов, которые склонны более эффективно использовать память.

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