Функционально-императивный язык программирования El и его реализация
А.А. Малявко, П.А. Журкин, Н. С. Нагорнов ФГБОУ ВО НГТУ, Новосибирск, Россия
Аннотация. В работе содержится краткое описание основных отличительных характеристик нового функционально-императивного языка программирования El, в качестве основного прототипа которого был использован язык Erlang. Рассматриваются такие характеристики, как: чистота пользовательских функций, отсутствие операций явного захвата/освобождения памяти и, одновременно, отсутствие глобальных переменных и сборщика мусора, вариативность мутабельности, т.е. возможности переприсваивания значений переменных, по выбору программиста, вариативность статической/динамической типизации переменных, примитивные числовые, в том числе неограниченной точности, и высокоуровневые составные типы данных - строки UNICODE, списки односвязные и двусвязные, кортежи, бинарники, векторы прямоугольные и ступенчатые, функции - и операции над ними, специальные виды индексных выражений при обработке векторов - индексные ссылки, перегрузка функций не только по именам, но и по сигнатурам, анонимные функции и функции высшего порядка, единая синтаксическая конструкция для условных выражений, переключателей и циклов. Рассматривается задача создания мультиплатформенной системы компиляции программ на языке El для ОС Linux и Windows и исполнения скомпилированных программ. Описываются состав, структура и алгоритмы функционирования исполняющей системы - совокупности, состоящей из транслятора исходного кода на язык ассемблера инфраструктуры компиляторов LLVM и runtime-библиотеки. Рассматриваются особенности лексического, синтаксического и семантического анализаторов транслятора, обусловленные совокупностью отличительных характеристик языка. Описываются некоторые используемые при трансляции структуры данных, представляющие объекты компилируемой программы, алгоритмы генерации псевдокода и преобразования псевдокода в объектный код. Рассматривается структура библиотеки runtime-поддержки процесса исполнения скомпилированной программы. Описывается текущее состояние разрабатываемого транслятора для языка El.
Ключевые слова: функциональная парадигма, императивная парадигма, языки программирования, компиляторы, лексический, синтаксический и семантический анализ, генерация кода
ВВЕДЕНИЕ
На кафедре вычислительной техники НГТУ разрабатывается функционально -императивный язык программирования El [1] и система программирования для него. Согласно общепринятой терминологии [2], система программирования - это совокупность средств разработки программ: транслятор (компилятор или интерпретатор, или и то и другое), редактор текстов программ, отладчик, возможно -интегрированная (на наш взгляд более точный термин - интегрирующая) оболочка, библиотеки стандартных и пользовательских функций, программная документация, и др. В этой работе вкратце описываются некоторые отличительные черты языка и рассматриваются структура и функционал основной части исполняющей системы языка - транслятора и runtime-библиотеки. Прочие компоненты системы программирования в проекте присутствуют, но их разработка еще впереди.
I. КРАТКИЕ СВЕДЕНИЯ ОБ ОСОБЕННОСТЯХ ЯЗЫКА ПРОГРАММИРОВАНИЯ EL
Язык программирования El был разработан под сильным влиянием своего прототипа - языка Erlang [3], но отличается от него (и многих других языков) рядом существенных характеристик. К числу наиболее важных свойств языка El, существенно влияющих на выбор © AUTOMATICS & SOFTWARE ENGINERY. 2018,
подхода к его реализации, можно отнести следующее.
■ Все пользовательские функции - чистые [4], т.е. не могут создавать побочного эффекта и являются детерминированными (естественно только в тех случаях, когда из них не вызываются встроенные или библиотечные функции, которые могут не быть детерминированными или обладать побочным эффектом). Это обеспечивается отсутствием глобальных переменных, указателей, передачей аргументов в функции и возвратом их результатов только по значению.
■ Нет операций явного захвата/освобождения памяти. В сочетании с тем, что все переменные, доступные в любой функции, локальны, это приводит к отсутствию процесса «сбора мусора». Состояние памяти в момент возврата из функции отличается от ее состояния в момент входа в функцию только значением выработанного ею результата, которое присваивается локальной переменной вызывающей функции. Платой за это, естественно, являются некоторые затраты времени на захват памяти для переменных в момент входа в функцию или блок и на возврат памяти в кучу при выходе из функции, но эти затраты распределены по всему времени выполнения функций программы и не могут вызывать эффекта «stop the world» [5].
■ Мутабельность (возможность переприсва-нивания значений переменным) вариативна по выбору программиста. Используемые функцией
переменные по умолчанию, как это принято в функциональных языках [6], иммутабельны (для явного указания, что переменная не является переприсваиваемой ее нужно объявлять с ключевым словом def), однако их можно превращать в мутабельные, просто объявляя с использованием ключевого слова var. Объявления переменных делаются либо для указания мутабельности, либо для указания типа данных (см. следующий пункт) и могут находиться только в начале блока - совокупности операторов, охваченных фигурными скобками и выполняемых строго последовательно в порядке их записи.
■ Типизация обрабатываемых данных также вариативна. Прежде всего - имеются примитивные и составные типы данных. К примитивным относятся численные (целые и вещественные) данные ограниченной точности - int, long, float и double. Составными называются числа произвольной точности (bigint и bigfloat), строки (string), списки (list), кортежи (tuple), векторы (vector), бинарники (binary), функции (function) и модули (module). Векторы (массивы однотипных элементов неизменяемого размера) могут быть как прямоугольными, так и ступенчатыми. С каждым из составных типов связан определенный перечень встроенных методов, необходимых для эффективного использования данных. Типы используемых переменных можно не объявлять. В этом случае переменные могут получать только значения составных типов, причем фактический тип переменной формируется динамически при вычислении выражений. В то же время существует возможность объявлять типы переменных статически путем указания имени нужного типа (int, double, list, tuple, ...) после ключевых слов var или def. Это позволит транслятору создавать эффективный код обработки данных примитивных типов, что может быть особенно важно для обработки векторов, содержащих элементы таких типов.
■ Для составных высокоуровневых типов данных (строки, списки, кортежи, бинарники, векторы, функции, модули) реализованы разнообразные операции их обработки и наборы встроенных функций. Для списков реализован доступ как с головы, так и с хвоста, аналогично расширены средства генерации списков (list comprehension).
■ Все операции, которые могут быть выполнены над отдельными элементами данных (сложение, умножение, и т. д.), применимы и к массивам таких элементов (векторам) в целом. Использование таких высокоуровневых операций для векторов в целом позволяет программисту сосредоточиться на существе алгоритма обработки данных и не отвлекаться на мелкие детали, такие как необходимость объявления индексных переменных для циклов обработки элементов массива, установки их начальных значений, контроля границ изменения индексов и т.д. Достижению этой же цели служат
разнообразные способы записи индексных выражений для векторов, близкие к математическим обозначениям и позволяющие организовывать неявную циклическую обработку нужных подмассивов любого вектора. В индексных выражениях можно использовать мнемонические обозначения нижней и верхней границ вектора, диапазоны и шаг изменения индексов. Например, следующий оператор умножает все элементы 1-й строки матрицы на коэффициент к: таМх[ I ] [ _ .. Л ] *= к;
Обозначение «..» в индексном выражении фактически организует неявный цикл перебора всех элементов вектора, находящихся в указанном диапазоне.
■ Индексные ссылки - еще один способ упрощения записи алгоритмов обработки векторов в целом по сравнению с чисто императивным стилем программирования. Если в одном операторе записаны несколько одинаковых индексных выражений, например:
агг[2 * I + 1] = а1рка[2 * I + 1] - beta[2 * I + 1]; то все, кроме первого, могут быть заменены ссылкой на него: агг[2 * I + 1] = а1рка[#] - beta[#]. Наряду с уменьшением текстуальной сложности записи операторов, обрабатывающих векторы, индексные ссылки, так же, как и индексные выражения, предоставляют программисту возможность организовывать неявную циклическую обработку.
■ В тексте одного модуля может присутствовать сколько угодно вариантов одноименных функций с одинаковыми перечнями аргументов. Компилятор строит результирующий код только для каждой первой (по тексту) из перегруженных таким образом функций. Наличие всех остальных может рассматриваться, например, как реализация версионного контроля или как средство упрощения отладки модулей путем перемещения исследуемой версии функции в начало цепочки перегруженных версий.
■ Любая функция с конкретным набором формальных аргументов может содержать произвольное количество различных тел - так называемых охраняемых блоков. Охраняемый блок - это последовательность операторов в фигурных скобках, перед которым по определенным правилам записано логическое выражение (последний блок в цепочке может не иметь охранного выражения). При вызове такой функции для фактического выполнения выбирается первый блок по тексту, для которого охранное выражение имеет истинное значение при данном наборе значений фактических аргументов.
■ Имеется возможность передавать функции в качестве аргументов и возвращать их в качестве результатов, т. е. писать функции как первого, так и высших порядков. Функции могут быть анонимными, их можно присваивать как значения переменным. Образуемые при таких операциях
замыкания также следуют принципу локальности обрабатываемых переменных.
■ Модули могут включать в себя произвольное количество функций. Доступность функций для других модулей регулируется ключевыми словами public и private. Модули могут просто импортироваться, либо могут использоваться для создания одного или нескольких экземпляров, различающихся значениями константных полей и становящихся значениями переменных, через которые осуществляется доступ к их функциям. Простой импорт модуля на самом деле тоже приводит к неявному созданию его экземпляра. Таким образом, в языке реализована возможность создавать и использовать объекты, но нет ни интерфейсов, ни наследования.
■ В языке имеется единственная управляющая конструкция by, позволяющая определять разветвления вычислений на две и большее количество ветвей (аналоги условных операторов и переключателей) и циклы. Имеются две разные формы этого оператора, отличающиеся способами формирования проверяемого условия и сопоставления его с образцами. Первая форма предполагает вычисление (впоследствии возможно перевычисление) одного значения, которое далее последовательно сопоставляется с несколькими образцами и, при совпадении, приводит к выполнению соответствующей ветки. Пример оператора ветвления в первой форме, вычисляющий очередное значение Сиракузской последовательности по значению очередного члена x:
by x % 2 {of 0 : x \= 2; of 1 : x = 3 * x + 1;} Во второй форме в каждой ветке вычисляется свое логическое значение, при истинности которого выполняются соответствующая последовательность операторов. Общее выражение может использоваться для начальной установки. Те же самые вычисления для Сиракузской последовательности могут быть записаны так:
by {when x % 2 == 0 : x \= 2; else x = 3 * x + 1;} Последний оператор легко может быть преобразован в цикл, вычисляющий все члены Сиракузской последовательности:
by { when x == 1 : break; when x % 2 == 0 : x \= 2;
again; else x = 3 * x + 1; again; }
Здесь:
□ обе ветки, модифицирующие значения x, дополнены оператором again, осуществляющим возврат на начало выполнения оператора by;
□ добавлена ветка с условием x == 1, обеспечивающая выход из цикла.
Кроме операторов break и again можно использовать оператор next, приводящий к выполнению следующей по тексту ветки без обработки условия ее выполнения. В форме break <label>; и again <label>; эти операторы могут быть использоваться для выхода или возврата на начало охватывающего оператора by, имеющего метку <label>. Отметим, что явное задание циклических вычислений в императивном стиле
может использоваться вместо более дорогостоящих рекурсивных вызовов в функциональном стиле, которые, впрочем, тоже доступны.
■ Ожидается, что тестирование и отладка программ на языке El будут проще, чем в чисто императивных языках. Как обычно для языков функциональной парадигмы, это обусловлено тем, что в языке El нет ни глобальных переменных, ни указателей, нет побочного эффекта у функций (за исключением некоторых встроенных методов и ряда функций стандартных модулей, осуществляющих ввод-вывод), все функции чистые. Любое значение любой переменной формируется только внутри данной функции. Если функция полностью отлажена, то на ее работоспособность ничто извне повлиять не может. За это, конечно, тоже есть плата -необходимость копирования значений при передаче любых аргументов в функции и при возврате результатов.
■ Реализованы вложенные блочные комментарии.
II. СТРУКТУРА ИСПОЛНЯЮЩЕЙ СИСТЕМЫ
По своей нацеленности на эффективную обработку разнообразных, в том числе примитивных типов данных, язык El следует отнести к группе преимущественно компилируемых языков [7], хотя в последующем для него предполагается создание и интерпретатора.
Существование многих различных платформ (Linux, Windows, MacOS, ...) приводит либо к необходимости реализации нескольких различных компиляторов для каждой из них, либо к решению создавать кроссплатформенный компилятор. Второй путь, очевидно, более предпочтителен, поэтому в качестве решения рассматривается двухэтапная компиляция: на первом этапе выполняется трансляция программы с языка El в платформно-независимое промежуточное представление, которое на втором этапе преобразуется в объектный код для целевой платформы с использованием специально предназначенного для этого средства. В качестве такого инструмента из двух основных известных средств GCC и LLVM выбрана инфраструктура проектирования компиляторов LLVM [8]. Эта инфраструктура реализует регистровую виртуальную машину и имеет собственный входной язык, называемый языком ассемблера. Разрабатываемый для языка El компилятор будет транслировать программы на язык ассемблера LLVM.
В связи с тем, что структуры данных и операторы языка El имеют довольно высокий уровень, прямое преобразование их в весьма низкоуровневые конструкции языка ассемблера LLVM будет весьма трудоемким и порождать громоздкий объектный код. Поэтому предлагается реализовать все необходимые структуры
данных и основные операции над ними в виде библиотеки высокоуровневых примитивов. Тогда из операторов в программе на исходном языке можно генерировать код на языке ассемблера, в основном содержащий вызовы библиотечных функций. Предполагается, что библиотека примитивов пишется на языке уровня С/С++, заранее компилируется под каждую из целевых платформ и ее объектный код размещается на том компьютере, который будет осуществлять компиляцию программы на языке Е1. На Рис. 1 отображена предложенная схема компиляции. Из кода исходной программы транслятором генерируется программа на языке ассемблера ЬЬУЫ, затем инфраструктура создает объектный модуль, после чего компоновщик объединяет его с библиотекой примитивов и создает исполняемый файл. Компоненты, подлежащие реализации при таком подходе (отмечены символом «*») - транслятор исходного кода в программу на языке ЬЬУЫ и библиотека примитивов поддержки языка.
Исполняемый файл
Рис. 1. Схема кроссплатформенной компиляции Е1-программы
Две ветви схемы, представленной на Рис. 1, должны взаимодействовать друг с другом через единый интерфейс - структуру обрабатываемых данных. В скомпилированной и выполняющейся программе каждая переменная должна храниться в виде совокупности метаданных, поскольку язык является динамически типизированным, а переменные могут быть как изменяемыми, так и не изменяемыми. Информация о типе данных переменной не всегда известна в момент компиляции, а возможность или невозможность изменения переменной, уже имеющей значение, влияет на способ выполнения операций, имеющихся в языке. Например, при использовании механизма сопоставления с образцом: в случае, если операнд сопоставления является изменяемым, то должно быть выполнено присвоение ему значения переменной-образца, в противном же случае должен быть выработан результат их сравнения (сначала типов, а затем и значений).
Предполагается хранить каждую переменную в виде структуры, содержащей всю информацию, необходимую как компилятору, так и исполнительной гипйте-системе:
□ текущий или предопределенный тип данных;
□ набор флагов, определяющих наличие или отсутствие какого-либо свойства (является ли переменная модифицируемой, имеет ли фиксированный тип данных, ...);
□ указатель на непосредственное положение значения переменной в памяти. Примерный вид этой структуры приведен на
Рис. 2 в виде иЫЬ-диаграммы. Для каждой операции над некоторыми объектами в исходном коде программы для любой пары типов, если операция бинарна, или любого типа, если она унарна, генерируется вызов соответствующей библиотечной функции, фактически выполняющей данную операцию.
Рис. 2. Типовая структура объектов El-программы
Описав структуру типов данных языка и разработав реализацию всех необходимых операций для всех типов, мы получим библиотеку, которая позволит реализовать все возможности языка. Транслятор при этом будет генерировать относительно простой объектный код.
III. ТРАНСЛЯТОР С ЯЗЫКА EL НА ЯЗЫК LLVM
Логическая схема трансляции является типовой и предполагает разбиение всего процесса преобразования программы на пять основных этапов:
1. Лексический анализ.
2. Синтаксический анализ.
3. Семантический анализ.
4. Оптимизация промежуточного представления.
5. Генерация объектного кода.
На каждом из этапов транслируемая программа преобразуется из одного представления в другое.
Технологически транслятор организован тоже по классической схеме. Весь процесс управляется синтаксическим анализатором, который вызывает все прочие компоненты по мере необходимости.
111.1. Лексический и синтаксический анализаторы
Эти две компоненты были построены с использованием пакета программ автоматизации проектирования трансляторов Вебтранслаб [9, 10]. Для этого были разработаны система регулярных выражений определения лексики и формальная грамматика определения синтаксиса языка El. Лексика включает в себя правила определения 110 токенов - отдельных слов (например, знаки операций «+», «+=», «++», ...) и групп слов (идентификаторы, числовые и строковые литералы, ...). Грамматика относится к классу контекстно-свободных £Ц1)-грамматик и в пользовательском представлении содержит около 120 правил (во внутреннем представлении пакета Вебтранслаб более 320 правил). Эти системы правил были преобразованы пакетом Вебтранслаб в тексты программ на языке С++, реализующие конечный автомат без памяти в качестве лексического анализатора и конечный автомат со стековой памятью в качестве синтаксического.
Лексический анализатор (далее - сканер), построенный пакетом Вебтранслаб, затем был существенно доработан в связи с необходимостью реализации функций макропроцессора, обработки директив включения файлов и блочных вложенных комментариев. Сканер - это функция, вызываемая синтаксическим анализатором и возвращающая одно целочисленное значение - код токена очередного слова, прочитанного из входного текста. Если это слово является частью группы слов (идентификаторы или литералы), то в отдельной строковой глобальной переменной сканер запоминает его как символьную последовательность для возможной дополнительной обработки синтаксическим анализатором или сохранения в информационных таблицах транслятора. Сканер, как обычно, является жадным, т. е. цепочку символов «+++» разобьет на слова так: «++» «+», а не так: «+» «++». Все незначащие последовательности символов входного текста, такие как разделительные пробелы, табуляции и комментарии (как однострочные, так и многострочные, т. е. блочные) сканер игнорирует, не преобразуя в токены.
Синтаксический анализатор (далее - парсер) реализован как стековый автомат с несколькими состояниями [10]. В настоящее время для текущей грамматики языка El количество состояний автомата составляет немногим более 900. Управляющая таблица такого автомата одномерна, каждая ее клетка соответствует отдельному состоянию и содержит следующие поля.
1. Множество выбора состояния. Это экземпляр внутреннего класса SelSet, содержащий все токены, допустимые в правильной программе на входе парсера, находящегося в данном состоянии. В описываемой реализации множества выбора
имеются не во всех состояниях, а только в тех, которые поставлены в соответствие нетерминалам из левых частей грамматических правил. Когда парсер попадает в состояние, соответствующее любому символу из правой части правила, допустимость текущего входного токена не проверяется. Как показано в [6], это не влияет на способность парсера обнаруживать синтаксические ошибки, но существенно ускоряет его работу.
2. Управляющие флажки - биты, конкретные значения которых для каждого состояния установлены преобразователем грамматики в автомат и от которых зависит поведение парсера. Как обычно в таких автоматах имеются следующие флажки:
□ A (Accept) - вызвать сканер для чтения следующего токена (этот флажок устанавливается в состояниях, соответствующих терминалам).
□ S (push return address to Stack) - занести на верхушку стека номер текущего состояния, увеличенный на единицу (построителем этот флажок устанавливается для всех состояний, соответствующих нетерминалам в правой части правила).
□ R (Return to state from stack) - переключить автомат в состояние, номер которого снимается с верхушки стека (этот флажок устанавливается в состояниях, соответствующих последним символам каждого правила). Флажки R и S не могут быть установлены оба в одном состоянии, они аннулируют друг друга.
□ E (suppress stop by Error) - подавить останов по ошибке, если текущий токен не принадлежит множеству выбора состояния (построителем автомата этот флажок устанавливается для всех состояний, соответствующих нетерминалу из левой части правила, кроме последнего).
3. Адрес перехода - номер состояния, в которое должен перейти автомат, если не установлен флажок R.
4. Указатель на функцию-действие, расширяющую функциональность парсера. Если его значение не равно NULL, то эта функция вызывается, в противном случае ничего не делается.
При запуске транслятора конечный автомат стартует в нулевом состоянии при пустом стеке и первом токене, прочитанном из входного текста. Далее его функционирование описывается следующим алгоритмом.
Шаг 1. Если множество выбора текущего состояния пусто, то выполняется переход к шагу 2, иначе проверяется, входит ли в это множество текущий токен. Если входит, то переход к шагу 2, иначе - проверяется значение флажка E. Если этот флажок в текущем состоянии установлен, то номер текущего состояния увеличивается на 1 и выполняется возврат на начало шага 1, иначе - запускается механизм нейтрализации синтаксических ошибок, описанный ниже.
Шаг 2. Проверяется значение флажка А (он устанавливается в единицу только для тех состояний, которые соответствуют терминалам в правых частях правил). Если флажок установлен, то сравниваются текущий токен и токен терминала и, если они совпадают, то вызывается сканер для чтения следующего токена и выполняется переход к шагу 3, иначе запускается механизм нейтрализации.
Шаг 3. Проверяется флажок и при его единичном состоянии на верхушку стека заносится номер текущего состояния, увеличенный на единицу. Проверяется флажок Я и, если он установлен, то номер следующего состояния снимается с верхушки стека (если стек пуст, то переход к шагу 4), иначе выбирается из текущей клетки, автомат переключается в это состояния, выполняется возврат к шагу 1.
Шаг 4. Автомат останавливается по концу проверки правильной программы, если текущий токен - EndOfFile (т. е. был прочитан весь входной текст) и по ошибке в противном случае.
В описанный алгоритм парсера встроен механизм полной нейтрализации синтаксических ошибок, целями реализации которого являются:
□ продолжение анализа входной программы после обнаружения синтаксических ошибок;
□ для каждой ошибки поиск такого варианта нейтрализации, при котором следующая ошибка находится на максимальном расстоянии от текущей;
□ обнаружение всех реально существующих в программе ошибок (максимальное количество ошибок до прекращения процесса анализа может быть задано в качестве параметра транслятора);
□ блокировка ложных сообщений о несуществующих ошибках.
В момент первичного обнаружения синтаксической ошибки (см. Шаг 1 и Шаг 2) инкрементируется счетчик количества ошибок (если его значение превышает установленный лимит, то работа транслятора прекращается), копируется состояние стека автомата, номер его текущего состояния и позиция текущего токена во входной программе. Формируется множество допустимых токенов. Это либо токен терминала из правой части правила (вход в режим нейтрализации выполнен из Шага 2), либо объединение множеств выбора текущего состояния со всеми множествами предыдущих состояний, имеющих установленный флажок Е (вход в режим нейтрализации выполнен из Шага 1). Поясним, что второй случай соответствует ситуации, когда синтаксическая ошибка обнаруживается при переборе правил для замены нетерминала в момент проверки вхождения текущего токена в множество выбора последнего правила.
Теперь собственно алгоритм нейтрализации. Вначале организуется перебор всех допустимых токенов для вставки. Каждый токен из множества допустимых заносится в качестве
текущего токена из программы, восстанавливается текущее состояние и стек автомата, автомат перезапускается. Если автомат останавливается по ошибке на первом токене, прочитанном после перезапуска из текста входной программы, то попытка нейтрализации считается неудавшейся. Если же автомат останавливается по ошибке на более удаленном символе или вообще по концу файла, то попытка считается удавшейся. Она запоминается как наиболее удачная, если расстояние от точки останова до места первичной ошибки для нее больше, чем у всех предыдущих.
Затем аналогичным образом организуется замена ошибочного токена путем перебора всех токенов из множества допустимых с фиксацией наиболее удачной попытки.
И, наконец, делается попытка нейтрализации ошибки путем удаления токена, вызвавшего первичный останов. Она тоже может быть зафиксирована, как наиболее удачная при отсутствии останова на следующем после ошибочного входном токене и выполнении вышеописанного условия по расстоянию.
Если хотя бы одна из попыток нейтрализации оказалась удачной, то для нее восстанавливается состояние автомата и его стек и выполняется выход из режима нейтрализации ошибок.
В противном случае включается режим переполоха. В данной реализации он состоит в чтении и игнорировании токенов из входной программы до тех пор, пока очередной из них не окажется принадлежащим множеству допустимых токенов. В этот момент (или по получения токена ЕпёО;^Ие) выполняется выход из режима нейтрализации ошибок.
Парсер, построенный по исходной грамматике языка Е1, сам по себе только определяет синтаксическую (и лексическую путем использования сканера) правильность входной программы. Для расширения его функциональности с целью реализации остальных этапов процесса трансляции, в грамматику вставляются действия - фрагменты программ на языке С++, оформляемые построителем пакета Вебтранслаб в виде функций, указатели на которые размещаются в клетках управляющей таблицы автомата. Вызовы этих функций будут выполняться по мере движения автомата по состояниям в процессе проверки правильности транслируемой программы. Такое расширение грамматики языка позволяет одновременно с процессами лексического и синтаксического анализа реализовывать последующие этапы трансляции -преобразование входной программы в промежуточную форму (постфиксную запись либо абстрактное синтаксическое дерево), дальнейшее преобразование в псевдокод, семантический анализ, оптимизацию, генерацию объектного кода (в данном случае - кода на языке ассемблера ЬЬУЫ).
111.2. Генератор псевдокода
Псевдокод - промежуточная форма программы, которую строит транслятор для фиксации той последовательности операций, которую должен будет выполнять компьютер. Инструкции псевдокода далее преобразуются в соответствующие им инструкции целевого объектного кода. Это значит, что структура псевдокода компилируемой программы должна соответствовать структуре конечного целевого кода. Структура исходной программы может значительно отличаться от структуры программы, полученной в результате трансляции.
Выделим суть различий в структуре исходной программы на языке El и генерируемого объектного кода. Как было отмечено ранее, в языке допускается перегрузка функций, имеющих одинаковые имена. Выбор той или иной версии перегруженной функции для вызова осуществляется на основании сопоставления фактических аргументов списку формальных параметров каждой из версий функции, а также вычисления их охранников. В описываемом трансляторе данный механизм реализуется в виде дополнительной, так называемой обрамляющей функции. При вызове функции, которая многократно перегружена, фактически вначале вызывается эта служебная функция, она выполняет все необходимые сопоставления аргументов, вычисления выражений-охранников и выбирает подходящую версию тела функции и осуществляет его вызов.
Перед генерацией псевдокода требуется выявить и зафиксировать в промежуточном представлении ту последовательность действий в исходной программе, которая фактически должна выполняться. В исходном тексте программы эта последовательность редко бывает отображена явно (например, обыкновенные арифметические операции обычно записаны в инфиксной форме, порядок вычислений, предписываемый правилами арифметики, не совпадает с порядком, в котором записаны знаки операций, он может изменяться с помощью круглых скобок и т.д.). Для того, чтобы выявить ту последовательность операций, в которой они фактически должны выполняться, программа преобразуется в одну из промежуточных форм - абстрактное синтаксическое дерево (АСД) или постфиксную запись (ПФЗ). В разрабатываемом трансляторе для языка El используется ПФЗ.
Для преобразования входной программы в ПФЗ исходная грамматика языка El была расширена путем вставки в правила действий, которые, выполняясь друг за другом по мере продвижения автомата по правилам, обрабатывают текущие токены с использованием вспомогательных структур данных.
ПФЗ является тоже последовательностью токенов, как и исходная программа, но в ней отсутствуют скобки, меняющие порядок выполнения операций и другие токены,
требовавшиеся по правилам языка для разметки программы. Каждый токен в ПФЗ может быть либо знаком операции, либо именем операнда.
Последующее преобразование ПФЗ в псевдокод происходит следующим образом:
Создается пустой стек операндов. Из постфиксной записи извлекаются токены по одному. Если текущий токен - имя операнда, он помещается в стек. Если текущей токен - знак операции, то из стека извлекаются операнды в количестве, равном арности операции. Если операция вырабатывает какой-либо результат, для него формируется имя и помещается в стек операндов для обработки следующими операциями. Совокупность знака операции, операндов и имени вырабатываемого результата образует одну инструкцию псевдокода.
Приведенный алгоритм достаточно прост и служит основой для генератора псевдокода. Чтобы учесть описанное выше различие в структуре исходного кода программы и генерируемого объектного кода, алгоритм требуется дополнить.
Во-первых, псевдокод генерируется не полной последовательностью инструкций, соответствующей всей транслируемой программе, а блоками. Это нужно по той причине, что часть инструкций, например, отвечающих за создание списка формальных параметров, должна быть включена как в состав кода конкретной функции, так и в код обрамляющей функции (для попытки выполнения сопоставления). Некоторые инструкции попадут только в код самой функции (инструкции непосредственно тела функции), а некоторые только в код обрамляющей функции (инструкции для вычисления охранников).
Если генерировать код блоками, в конце его можно достаточно просто скомпоновать в требуемой последовательности. На Рис. 3 приведена предлагаемая структура хранения инструкций компилируемого модуля. Модуль содержит набор функций, каждая из которых представлена набором версий. Каждая версия содержит блок инструкций тела функции, блок инструкций объявления аргументов, блок инструкций для вычисления выражений охранников.
Рис. 3. Структура, хранящая псевдокод программного модуля
Во-вторых, постфиксная запись должна содержать дополнительные знаки операций,
которые не транслируются в инструкции целевого объектного кода и не попадают в псевдокод, а являются управляющими инструкциями для генератора псевдокода. Эти управляющие инструкции позволят генератору формировать описанную выше структуру для хранения псевдокода: создавать новые функции, разбивать на версии, создавать новые блоки, определять блок, в который требуется поместить очередную полученную из ПФЗ инструкцию.
В-третьих, для работы с хранящей псевдокод блочной структурой удобно использовать конечный автомат, в котором состояния указывают на блок, с которым генератор псевдокода работает в данный момент, а в качестве входных символов использовать управляющие инструкции для генератора. Структура конечного автомата приведена на Рис. 4.
Рис. 4. Диаграмма состояний генератора псевдокода
Пусть генератор псевдокода находится в начальном состоянии. Получив из ПФЗ инструкцию определения функции (назовем ее defineFunction) - ее операндом является имя функции, проверяет, есть ли уже в модуле функция с таким именем. Если такой функции нет, она добавляется в структуру, и для нее создается первая версия. Если такая функция уже есть, для нее просто создается новая версия. Автомат переходит в состояние «Формирование списка аргументов». Находясь в этом состоянии, все инструкции генератор помещает в блок «Код объявления аргументов». По инструкции окончания создания списка аргументов ^А^), автомат переходит в состояние готовности.
Далее по тексту программы может следовать охранник, тогда в ПФЗ появляется инструкция beginGuard. В этом случае автомат переходит в состояние «Охранник», это означает, что все извлекаемые из ПФЗ инструкции помещаются в блок «Код вычисления охранника».
Из состояний «Охранник» и «Готовность» осуществляется переход в состояние «Тело», когда появляется инструкция beginBody, которая соответствует началу тела функции в исходном коде. В этом состоянии все инструкции попадают в блок тела функции.
Из состояния «Тело» автомат переходит в состояние готовности, поскольку согласно синтаксису языка Е1, вслед за телом функции может следовать новый охранник и соответствующая ему реализация (т. е. тело) функции.
Конец функции соответствует инструкции endFunction. По ней автомат переходит в начальное состояние, т.е. генератор псевдокода готов создать новую функцию, или новую перегрузку уже определенной в модуле функции.
Переход из состояния готовности в какое-либо другое состояние кроме начального означает, что в модуле присутствует несколько перегрузок одной и той же функции с одним списком аргументов, но разными охранниками. Поэтому такой переход сопровождается созданием новой версии текущей функции с копированием блока аргументов.
В процессе формирования псевдокода каждой конкретной версии каждой функции транслятор ведет таблицу переменных. Это нужно по той причине, что в языке Е1 объявлять переменные перед их использованием не обязательно. Однако генерировать инструкции захвата памяти для переменных и их инициализации нужно, причем они должны быть помещены в начало функции, т.е. до их использования. Поскольку не все переменные заранее объявляются, то транслятор не может сгенерировать инструкции их создания до того, как просмотрит тело функции целиком. Использование указанной блочной структуры для хранения псевдокода позволяет достаточно просто добавить в начало функции код создания переменных из таблицы после того, как весь оставшийся код уже создан.
После того, как из ПФЗ были извлечены все токены, транслятор из имеющихся блоков компонует окончательный вариант псевдокода. Это происходит следующим образом.
Перебираются все функции в модуле.
Для каждой функции перебираются все ее созданные версии. При компоновке псевдокода текущей версии функции сначала генерируются инструкции для создания и инициализации использующихся в теле переменных. К ним присоединяется блок объявления аргументов, затем блок тела.
После того, как псевдокод всех версий функции был скомпонован, формируется псевдокод ее обрамляющей функции.
При этом снова перебираются все версии функции. Из текущей обрабатываемой версии берется блок инструкций объявления аргументов и добавляется к коду обрамляющей функции. После этого в конец псевдокода помещается инструкция tryЫatchArg - при ее выполнении должна осуществляться попытка сопоставления формальных параметров текущей версии функции фактическим аргументам.
Следом к псевдокоду обрамляющей функции присоединяется блок вычисления выражения-охранника данной версии функции и инструкция
checkArgGuard, при выполнении которой осуществляется проверка значения выражения-охранника на истинность.
Таким образом, после обработки всех имеющихся в модуле функций, получается полный псевдокод модуля, пригодный к генерации объектного кода.
111.3. Генератор объектного кода
Полученный на предыдущих этапах трансляции псевдокод в последующем подвергается преобразованию в исходный код на языке ассемблера LLVM, после чего компилятор LLVM создает объектный модуль для целевой платформы.
Псевдокод представляет собой
последовательность инструкций для некой виртуальной машины. Он построен таким образом, чтобы из каждой инструкции можно было сформировать четко определенную последовательность инструкций для виртуальной машины LLVM.
Генерация кода LLVM начинается с того, что в начало файла помещается блок деклараций. Как уже было отмечено ранее, многие возможности языка реализованы в виде функций и находятся в библиотеке, которая позже компонуется с полученным объектным файлом. Таким образом, сгенерированный LLVM-код содержит в себе вызовы библиотечных функций, поэтому компилятор LLVM должен знать их имена, сигнатуры и типы возвращаемого значения. Блок декларации содержит объявление всех функций из библиотеки языка.
Далее генерируется код функций модуля. Каждая скомпилированная функция принимает два аргумента. Первый - указатель на переменную-кортеж, содержащий фактические аргументы. Второй - указатель на переменную, куда должно быть помещено возвращаемое значение. Эти переменные создаются транслятором и недоступны из пользовательского кода. Кроме того, данные, переданные в функцию по указателю, копируются в локальные переменные при выполнении операции сопоставления с образцом. Таким образом, отсутствие побочных эффектов и чистота функций не нарушаются.
При трансляции оператора вызова функции образуются операции псевдокода, которые формируют новый кортеж, состоящий из передаваемых фактических параметров. При генерации кода LLVM из инструкции вызова callFunction, размещенной в псевдокоде, указатель на переменную-кортеж передается в вызываемую функцию.
Также при преобразовании оператора вызова функции во внутреннее представление в псевдокоде генерируется создание специальной переменной, в которую будет помещено возвращенное функцией значение. Указатель на нее передается вторым аргументом. Изначально, переменная инициализируется значением,
эквивалентным специальному значению языка El - nothing. Если возникает ситуация, кода функции по завершении нечего вернуть, она просто передает управление вызвавшей ее функции, таким образом, возвращенное из функции значение будет nothing.
Причина, по которой значение возвращается через указатель на служебную переменную, хранящуюся во фрейме функции, из которой была вызвана текущая, связана с реализацией сложных типов данных языка El. Пусть в программе есть вызов функции: x = func(10, 20);
Результат трансляции этого оператора
приведен в листинге 1._
Листинг 1. Результат трансляции вызова функции
1 // reg2 - кортеж фактических параметров
2 // const. 1 - литеральная константа 10
3 // const.2 - литеральная константа 20
4 ...
5 // Формируем кортеж параметров
6 call void @dt_tuple_append_element(%var.type* %reg2 , %var. type * %const. 1)
7 call void @dt_tuple_append_element(%var.type* %reg2 , %var. type * %const. 2)
8 ...
9 // reg3 - переменная, куда будет помещено возвращаемое значение
10 // Вызов функции func с передачей кортежа фактических параметров и указателя, по которому нужно разместить возвращаемое значение
11 call void @caller.func(%var.type* %reg2, %var.type* %reg3)
Сначала формируется переменная-кортеж, содержащая фактические параметры. В строках 6 и 7 вызывается библиотечная функция, добавляющая в конец кортежа элемент. Этой функции передается указатель на переменную-кортеж и указатель на значение литеральной константы. В строке 11 вызывается сама функция (точнее ее обрамляющая функция), и ей передается указатель на сформированную переменную-кортеж с параметрами (reg2) и указатель на переменную, в которую нужно поместить возвращаемое значение (reg3).
В разделе «Генератор псевдокода» описано, как генерируется обрамляющая функция, которая осуществляет поочередное сопоставление значений фактических аргументов формальным параметрам одноименных функций и вычисление их предохранителей для выбора той функции-перегрузки, которая соответствует фактическим аргументам и которую в действительности следует вызвать.
Листинг 2. Перегруженные функции на языке
El
1 func(A, B, 0) {...}
2 func(A, B, C) {...}_
Листинг 3. Результат компиляции
перегруженных функций в язык LLVM
1 define void @inst.1.func(%var.type* %f.arg, %var.type* %f.ret) {...}
2 define void @inst.2.func( %var.type* %f.arg, %var.type* %f.ret) {...}
3 define void @caller.func(%var.type* %f.arg, %var.type* %f.ret) {...}
В листингах 2 и 3. показано, во что транслируются перегруженные функции. Одноименные функции /гпс получают имена inst.1.func и inst.2.func, а также генерируется обрамляющая функция сашг./тс. Три появившиеся функции принимают два аргумента -указатель на кортеж фактических параметров /.аг^) и указатель на переменную, в которую будет помещен результат, возвращаемый функцией (/.^г).
Псевдокод обрамляющей функции содержит инструкцию tryыatcharg. В результате трансляции этой инструкции должен быть сгенерирован код, содержащий:
1. Попытку сопоставления аргументов;
2. Проверку результата сопоставления;
3. Условный переход в зависимости от результата либо к блоку вычисления охранника, либо к блоку сопоставления с кортежем формальных параметров следующей версии функции.
За инструкцией tryыatcharg следует набор инструкций, реализующих вычисление значение выражения-охранника, после чего идет инструкция checkargguard. В результате трансляции этой инструкции, должен быть сгенерирован код, содержащий:
1. Проверку результата вычисления значения-охранника;
2. Условный переход в зависимости от результата либо к блоку вызова текущей проверяемой версии функции, либо к блоку сопоставления с кортежем формальных параметров следующей версии функции. Кроме того, сам блок вызова выбранной
версии функции также дожжен быть сгенерирован. Этот блок содержит:
1. Инструкцию непосредственного вызова выбранной версии функции;
2. Инструкцию безусловного перехода к концу тела обрамляющей функции. Она необходима, т. к. после работы нужной версии функции, управление будет передано назад в обрамляющую функцию, а т. к. нужная версия уже была вызвана, остальные перебирать не требуется, и нужно вернуться назад к той точке программы пользователя, где был осуществлен вызов перегруженной функции, предварительно выполнив сборку локального мусора обрамляющей функции.
В листинге 4 приведен пример трансляции инструкции tryыatcharg на язык ассемблера
ЬЬУМ. На данный момент в трансляторе не реализована поддержка предохранителей, поэтому генерируемый код пока что не содержит блоков вычисления и проверки значения предохранителей.
Листинг 4. Результат трансляции инструкции
псевдокода tryMatchArg
1 // Попытка выполнить сопоставление и
сохранение результата
2 %match.1.res = call i32 @dtype variable match(
%var.type* %v1.reg1,
%var.type* %f.arg)
3 // Проверка результата сопоставления и
условный переход
4 %match.1.succ = icmp eq i32 %match.1.res, 1
5 br i1 %match.1.succ, label %call.1, label
%match.2
6 call.1:
7 // Вызов нужной версии функции
8 call void @instance.1.func( %var.type* %f.arg,
%var.type* %f.ret)
9 //Выход из обрамляющей функции
10 br label %Func Exit
В строке 2 вызывается библиотечная функция dtype_variable_match, которая выполняет сопоставление и возвращает в случае успеха true, иначе - false.
В строке 4 осуществляется проверка результата сопоставления на равенство единице.
В строке 5 осуществляется условный переход либо на метку call.1 - к блоку вызова текущей проверяемой версии функции, либо на метку match.2 - к сопоставлению с аргументами второго экземпляра перегруженной функции.
Строка 6 начинает блок вызова текущей проверяемой версии функции и содержит метку для перехода к этому блоку. В строке 8 расположен непосредственный вызов экземпляра перегруженной функции. Строка 10 содержит безусловный переход в конец обрамляющей функции.
Все остальные инструкции псевдокода (на данном этапе реализации языка) транслируются в объектный код достаточно просто, поскольку имеют соответствие инструкциям LLVM либо реализованным библиотечным функциям, и в результате трансляции превращаются в набор соответствующих инструкций или вызовов библиотечных функций.
III.4. Runtime-библиотека
Библиотека языка содержит в себе реализацию сложных типов данных, а также различные функции, реализующие языковые операции над объектами этих типов. Библиотека реализуется с использованием языка программирования C.
Атрибуты переменной - ее тип и (им)мутабельность влияют на то, как будут выполняться операции над ней. Поэтому для каждой переменной нужно хранить информацию
об атрибутах. Переменная представлена в памяти структурой, содержащей различные метаданные, необходимые для ее обработки, а также указатель на расположенное в куче значение. Объекты являются динамическими, это сделано по той причине, что они имеют сложную структуру и постоянно меняются в ходе выполнения программы. В листинге 5 приведено описание структуры переменной в памяти.
Листинг 5. Структура, представляющая переменную в памяти программы
1. typedef struct
2. {
3. uint8_t flags;
4. uint8_t type;
5. void* value;
6. } TVARIABLE;
Поле flags - биты этого поля отвечают за наличие или отсутствие некоторого свойства переменной (на данный момент поддерживаются иммутабельность и возможность изменения переменной своего типа). Поле type хранит целое число, кодирующее тип данных языка. Поле value является указателем на значение переменной в памяти.
На Рис. 5 приведена схема размещения в памяти программы переменной типа list - список (в данном случае содержащий 3 элемента). Как видно из рисунка, структура, описывающая переменную, расположена в стеке и ссылается на значение, хранящееся в куче.
Т VARIABLE
Рис. 5. Схема размещения объекта типа list в памяти
Когда структура TVARIABLE создана в памяти, ее поля нужно инициализировать. Поля могут быть инициализированы по-разному в зависимости от того, как в транслируемой программе была объявлена переменная. Для инициализации полей структуры в библиотеке имеется набор функций - каждая соответствует одному способу объявления переменной. Оператор объявления переменной в программе на El транслируется в вызов соответствующей функции инициализации структуры T_VARIABLE.
Инициализация полей структуры TVARIABLE не означает, что инициализирована сама переменная. Все функции, инициализирующие
структуру обращают указатель value в NULL. Чтобы с переменной можно было работать, необходимо сконструировать в памяти объект, который будет являться значением переменной, а затем присвоить указатель на этот объект в поле value структуры T_VARIABLE. Для этих целей в библиотеке также имеется отдельная функция, вызов которой осуществляется из генерируемого компилятором кода LLVM.
Таким образом, создание переменной происходит в 2 этапа - создание и инициализация структуры T_VARIABLE, затем конструирование начального значения переменной и связывание его со структурой, описывающей переменную. Инициализация переменной может быть опущена, если она отсутствует в исходном коде транслируемой программы.
Неинициализированная переменная называется несвязанной (аналог unbound variable в Эрланге).
При выходе из блока операторов или из функции, должна осуществляться сборка локального мусора. Для каждого из имеющихся в языке типов данных в библиотеке присутствует своя версия, освобождающая память, занимаемая объектом этого типа. В конце блока операторов, для каждой локальной переменной транслятор El генерирует вызов функции для освобождения памяти, занимаемой этой переменной.
В языке используется так называемое позднее связывание. Как было сказано ранее, язык является динамически типизированным, поэтому транслятору не всегда известно, какой тип будет иметь та или иная переменная в момент выполнения программы, а значит, неизвестно, как выполнять те или иные операции над объектом. Позднее связывание реализовано следующим образом.
Каждая языковая операция реализована для каждого из типов операндов в виде отдельной функции. В том месте программы, где должна выполняться некоторая операция, транслятор подставляет вызов библиотечной функции, которая в зависимости от типа операндов (напомним, что тип хранится в поле type структуры TVARIABLE) вызывает функцию, реализующую данную операцию над объектами указанного типа. В листинге 6 приведен пример функции для освобождения памяти переменной, определяющей тип операнда и вызывающей соответствующую функцию.
Листинг 6. Пример реализации позднего связывания
1 void dtype_variable_destroy( T VARIABLE* var )
2 {
3 ...
4 switch (var-> type)
5 {
6 case DT_TUPLE:
7 dtypetupledestroy(var);
8 break;
9
10 case DT INT:
11 dtype int destroy(var);
12 break;
13
Транслятор, когда это требуется, если он не знает, какой тип данных имеет переменная, подставляет вызов функции
dtype_variable_destroy и передает указатель на переменную, память которой нужно освободить. Эта функция посредством переключателя определяет тип данных переменной и вызывает соответствующую функцию для освобождения памяти объекта конкретного типа данных.
ЗАКЛЮЧЕНИЕ
Разрабатываемый на кафедре вычислительной техники НГТУ язык программирования El имеет свойства и характеристики двух парадигм: функциональной и императивной. Мультипара-дигменность и особенности ряда предложенных в языке решений приводят к некоторым проблемам и трудностям разработки структуры и функции-онала мультиплатформенной компилирующее-исполняющей системы. Варианты и способы решения возникающих при разработке этой системы проблем представлены в данной работе. Описаны основные алгоритмы лексического, синтаксического и элементов семантического анализа и генерации объектного кода.
Мультиплатформенность проще всего реализовать путем двухэтапной компиляции с использованием на втором этапе существующей инфраструктуры компиляторов LL VM.
Высокоуровневые операторы языка целесообразно реализовать в виде runtime-библиотеки функций, а при трансляции программы на первом этапе генерировать вызовы этих функций. Скомпилированный объектный модуль далее компонуется с библиотекой.
Разрабатываемый транслятор после завершения его отладки предполагается представить как свободное программное обеспечение в рамках концепции «Open Source» с размещением исходных кодов на соответствующих серверах в Интернете.
ЛИТЕРАТУРА
[1] Себеста P. Основные концепции языков программирования - М.: «Вильямс», 2001.
[2] Кауфман В.Ш. Языки программирования. Концепции и принципы. - М., ДМК-пресс, 2011. -464 с.
[3] Armstrong J. Programming Erlang: Software for a Concurrent World. 2nd Edition. The Pragmatic Bookshelf,. Dallas, USA, 2013.
[4] Филд А., Харрисон П. Функциональное программирование - М.: Мир, 1993. - 637 с.
[5] Десять решений проблемы stop the world при использовании автоматической сборки мусора -Блог о программировании и пр. Режим доступа: https:// eax. me/stop-the-world/ (Дата обращения 12.01.2018)
[6] Wadler P. Why no one uses functional languages, ACM SIGPLAN Notices, August 1998
[7] Орлов С. А. Теория и практика языков программирования. Учебник для вузов. Стандарт 3-го поколения. - СПб., Питер, 2013. - 688 с.
[8] Лопес Б.К, Аулер Р. LLVM: инфраструктура для разработки компиляторов. / - М.: ДМК Пресс, 2015. - 342 с.
[9] Малявко А. А. Использование веб-приложений и веб-технологий при разработке учебного программного обеспечения для изучения методов трансляцию, Материалы Международной научно-методической конференции "Современное образование: технические университеты в модернизации экономики России". - Томск, Изд-во ТУСУР, 2011 г, С 45-46.
[10] Малявко А. А. Формальные языки и компиляторы: учебник. - Новосибирск: Изд-во НГТУ, 2014. - 431 с. - (Серия «Учебники НГТУ»).
Александр Антонович Малявко - доцент кафедры вычислительной техники НГТУ,
доцент, кандидат технических наук.
E-mail a.malyavko@corp.nstu.ru
630073, Новосибирск, просп. К.Маркса, д. 20
Павел Андреевич Журкин -
студент 4-го курса факультета автоматики и вычислительной техники НГТУ. E-mail zhur.pav@yandex.ru
630073, Новосибирск, просп. К.Маркса, д. 20
Никита Сергеевич Нагорнов -
студент 4-го курса факультета автоматики и вычислительной техники НГТУ. E-mail neoron95@gmail.com
630073, Новосибирск, просп. К.Маркса, д. 20
Статья поступила в редакцию 23 января 2018 г.
Functionally Imperative Programming Language El and its Implementation
A.A. Malyavko, P.A. Zhurkin, N.S. Nagornov
Novosibirsk State Technical University, Novosibirsk, Russia
Abstract. The paper contains a brief description of the main distinguishing characteristics of the new functionally- imperative programming language El, the main prototype of which was the Erlang language. We consider such characteristics as: users pure functions, absence of operations for explicit memory capture / deallocation, and, simultaneously, absence of global variables and garbage collector, variability of mutability, i.e. the possibility of reassigning the values of variables, at the choice of the programmer, the variability of static / dynamic typing of variables, primitive numerical, including unlimited accuracy, and highlevel composite data types - UNICODE strings, simply-connected and doubly-connected lists, tuples, binaries, rectangular and stepwise vectors, and operations on them, special types of index expressions when processing vectors - index references, overloading of functions not only by names, but also by signatures, anonymous and higher-order functions, unified syntax for conditional statements, switches and cycles. The task of creating a multi-platform system for compiling El programs for Linux and Windows and executing compiled programs is considered. The composition, structure and algorithms of functioning of the executing system - a set consisting of a source code translator into the language of the assembler of the LLVM compilers infrastructure and the runtime library are described. The peculiarities of the lexical, syntactic and semantic analyzers of the translator are considered, due to the set of distinctive characteristics of the language. Describes some data structures used for translation that represent the objects of the compiled program, algorithms for generating a pseudocode, and converting a pseudocode into an object code. The structure of the runtime-support library for the compiled program execution process is considered. The current state of the developed translator for the El language is described.
Key words: functional paradigm, imperative paradigm, programming languages, compilers, lexical, syntactic and semantic analysis, code generation
REFERENCES
[1] Sebesta P. Osnovnye koncepcii jazykov programmirovanija - M.: «Vil'jams», 2001.
[2] Kaufman V.Sh. Jazyki programmirovanija. Koncepcii i principy. - M., DMK-press, 2011. - 464 s.
[3] Armstrong J. Programming Erlang: Software for a Concurrent World. 2nd Edition. The Pragmatic Bookshelf,. Dallas, USA, 2013.
[4] Fild A., Harrison P. Funkcional'noe programmirovanie - M.: Mir, 1993. - 637 s.
[5] Desjat' reshenij problemy stop the world pri ispol'zovanii avtomaticheskoj sborki musora -- Blog o programmirovanii i pr. Rezhim dostupa: https://eax.me/stop-the-world/ (Data obrashhenija 12.01.2018)
[6] Wadler P. Why no one uses functional languages, ACM SIGPLAN Notices, August 1998
[7] Orlov S.A. Teorija i praktika jazykov programmirovanija. Uchebnik dlja vuzov. Standart 3-go pokolenija. - SPb., Piter, 2013. - 688 s.
[8] Lopes B.K, Auler R. LLVM: infrastruktura dlja razrabotki kompiljatorov. / - M.: DMK Press, 2015. -342 s.
[9] Maljavko A. A. Ispol'zovanie veb-prilozhenij i veb-tehnologij pri razrabotke uchebnogo programmnogo obespechenija dlja izuchenija metodov transljaciju., -Materialy Mezhdunarodnoj nauchno-metodicheskoj konferencii "Sovremennoe obrazovanie: tehnicheskie universitety v modernizacii jekonomiki Rossii", -Tomsk, Izd-vo TUSUR, 2011 g, S 45-46.
[10] Maljavko A. A. Formal'nye jazyki i kompiljatory: uchebnik. - Novosibirsk: Izd-vo NGTU, 2014. - 431 s. - (Serija «Uchebniki NGTU»).
Alexander Antonovich Malyavko
is an associate professor of the Computer Science Department at the NSTU, an associate professor, candidate of technical sciences. E-mail a.malyavko@corp.nstu.ru
630073, Novosibirsk, str. Prosp. K. Marksa, h. 20
Pavel Andreevich Zhurkin is a 4th
year student of the Faculty of Automation and Computer Science at the NSTU.
E-mail zhur.pav@yandex.ru
630073, Novosibirsk, str. Prosp. K. Marksa, h. 20
Nikita Sergeevich Nagornov is a 4th year student of the Faculty of Automation and Computer Science at the NSTU.
E-mail neoron95@gmail.com
630073, Novosibirsk, str. Prosp. K. Marksa, h. 20
The paper was received on January 23, 2018.