среда, 18 мая 2016 г.

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

Как уже было описано в разделе косвенности, язык MUMPS содержит определения синтаксических конструкций, не позволяющих выполнить систему выполнения кода по схеме традиционных компиляторов с одним только неизменяемым исполняемым кодом. Этому препятствуют и операции косвенности, и команда XECUTE.

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

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

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

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

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

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

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

Код операции Действие
op_const Взять следующий байт и, считая его номером константы, взять из секции констант значение и поместить его на стек.
op_plus Взять со стека два значения, сложить и поместить результат на стек.
op_mult Взять со стека два значения, умножить и поместить результат на стек.
op_horolog Вычислить значение даты и времени в формате \$HOROLOG и поместить значение на стек.
op_concat Взять со стека два значения, выполнить конкатенацию и поместить результат на стек.
op_writestr Взять со стека значение, привести к строке и вывести в текущее устройство.
op_writenl Вывести в текущее устройство перевод строки.
op_lenght_2 Взять со стека значение, привести к строке, вычислить длину и результат поместить на стек как значение.


Используя такую виртуальную машину, можно подать ей на трансляцию и затем на выполнение уже несколько заданий:
1) write $h,!
2) write 12+45*78,!
3) write 123_456_789,!
При трансляции строки
write $h,!
получаем последовательность байткодов:
1) op_horolog
2) op_writestr
3) op_writenl
При их последовательном выполнении сначала вычисляется значение $H, заносится на стек, затем значение вынимается со стека и выводится в текущее устройство, затем в текущее устройство выводится перевод строки.

При трансляции строки
write 12+45*78,!
получаем последовательность байткодов:
1) op_const
2) 1
3) op_const
4) 2
5) op_plus
6) op_const
7) 3
8) op_mult
9) op_writestr
10)op_writenl
и соответствующую ей таблицу констант:
1  12
2  45
3  78
При их последовательном выполнении виртуальная машина выполняет следующие действия при пустом начальном стеке вычисления:
  • op_const, 1: Берет константу номер 1 (значение по таблице = 12) и помещает на стек. Состояние стека
    12
    
  • op_const, 2: Берет константу номер 2 (значение по таблице = 45) и помещает на стек. Состояние стека
    45
    12
    
  • op_plus: Берет со стека два значения, состояние стека пусто (в данном примере записи на стеке кончились). Вычисляет сумму (получается 57) и помещает значение на стек, состояние стека
    57
    
  • op_const, 3: Берет константу номер 3 (значение по таблице = 78) и помещает на стек. Состояние стека
    78
    57
    
  • op_mult: Берет со стека два значения, состояние стека пусто (в данном примере записи на стеке кончились). Вычисляет произведение (получается 4446) и помещает значение на стек, состояние стека
    4446
    
  • op_writestr: Берет со стека значение, состояние стека - пусто. Значение приводится к строке и выводится в текущее устройство. Состояние стека - пусто.
  • op_writenl: Выводит в текущее устройство перевод строки, состояние стека - пусто.
Третий пример транслируется соответственно в последовательность байткодов:
1) op_const
2) 1
3) op_const
4) 2
5) op_concat
6) op_const
7) 3
8) op_concat
9) op_writestr
10)op_writenl
и соответствующую таблицу констант:
1  123
2  456
3  789
Соответственно, при последовательном выполнении такого байткода со стеком вычисления производятся операции:
  1. На стек помещается значение 123.
  2. На стек помещается значение 456.
  3. Со стека снимаются два значения, стек пуст.
  4. На стек помещается значение 123456.
  5. На стек помещается значение 789.
  6. Со стека снимается два значения, стек пуст.
  7. На стек помещается значение 123456789.
  8. Со стека снимается значение.
Последнее снятое со стека значение выводится в текущее устройство, и следом выводится символ перевода строки.

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

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

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

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

У каждой виртуальной машины, если она не стандартизирована, определение таких операций собственное. Существуют относительно стандартизированные виртуальные машины исполнения интерпретаторов, например виртуальные машины Java, .NET, Lua. Для таких машин возможна трансляция других языков программирования в их байткод для того чтобы эти виртуальные машины использовались для выполнения как готовые. Для систем выполнения MUMPS такие стандарты на формат исполняемых бйткодов официально не поддерживаются и построение системы исполнения полностью определяет производитель MUMPS системы.

Сам генератор байткода по исходному тексту опирается на определение языка, данное для каждой синтаксической конструкции. Рассмотрим парсинг с кодогенерацией на примере разбора выражения. Выражение в языке MUMPS определено так:
expr := expratom [exprtail]
Это означает что для парсинга выражения (expr) необходимо сначала выполнить парсинг expratom, а затем, если есть символы, то парсинг exprtail. В свою очередь, определение expratom состоит из двух альтернатив:
expratom :=  | glvn      |
             | expritem  |
Здесь glvn - определение синтаксической единицы глобальной или локальной переменной, expritem - самостоятельная синтаксическая единица.

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

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

В рассматриваемом примере определение expritem состоит из перечня более детальных альтернатив, соответствующих вычислению значений:
expritem := | strlit           | константная строка
            | numlit           | константное число
     | exfunc           | пользовательская функция
     | exvar            | пользовательская переменная
     | svn              | системная переменная
     | function         | системная функция
     | unaryop expratom | унарный оператор с 
                          последующим expratom
     | (expr)           | открывающая круглая скобка, 
                          последующий элемент 
     в определении
     expr, с последующей 
     закрывающей круглой скобкой
Здесь две последних альтернативы определены рекурсивно, и парсер сначала должен определить есть ли символ унарного оператора, запомнить его, и перейти к парсингу последующего выражения как expratom. При его разборе считается что парсер выполняет кодогенерацию для этого элемента. Поэтому после возврата из парсинга expratom останется добавить к байткодам байткод унарной операции.

Например, при парсинге выражения
+$H
Сначала выполняется запоминание унарной операции плюс (+), затем кодогенерация для вычисления $H, затем добавляется байткод для вычисления унарной операции плюс. При выполнении байткода система исполнения уже выполнит действия в правильной последовательности, а не так как это написано - сначала оператор плюс а потом вычисление $H.

То же правило относится к альтернативе (expr) - эта синтаксическая конструкция может целиком заменять конструкцию expritem.

Определение конструкции exprtail дано так:
exprtail := | | binaryop   | expratom |
            | | [']truthop |          |
     | ['] ? pattern           |
К синтаксическим конструкциям binaryop относятся бинарные операторы, например +, -, \#. К синтаксическим конструкциям truthop относятся логические операторы, например >, =, <.

Таким образом, в языке MUMPS при трансляции выражения
a + b * c
Сначала выполняется вычисление a, затем вычисление b, затем сложение, затем вычисление c, затем умножение, а при трансляции выражения
a + ( b * c )
сначала вычисляется a, затем b, затем c, затем произведение b * c, затем сложение.

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

В полный набор операций виртуальной машины входит кроме вычисления имен, меток и значений, также множество дополнительных служебных операций, например при трансляции кода
write:a+b c+d,! set ...
виртуальная машина должна сначала вычислить постусловие (a+b), а затем, если оно не выполнилось, пропустить в байткоде выполнение кода для
c+d,!
для команды write и перейти сразу к вычислению аргумента следующей за ней команды set (или его постусловия, если оно было задано).

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

Современные интерпретаторы MUMPS систем зачастую поддерживают кроме предопределенного функционала также возможность вызова внешних модулей. Обычно это выполняется динамической загрузкой динамических библиотек. В этом случае DLL (или SO в Linux) должна реализовать определенный интерфейс для того, чтобы интерпретатор мог выполнить вызов.

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

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

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

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

Кроме того, для многих современных интерпретаторов MUMPS систем возможно выполнение декомпилятора байткода с получением исходного текста. Возможно, исходный текст не будет в точности повторять оригинальный, например декомпилятор может не определить в каком регистре была написана команда и была ли использована полная или сокращенная форма. Как правило, от версии к версии производители могут добавлять к определению байткода дополнительные элементы и декомпиляторы, тем самым, устаревают. Но в каких-то случаях они могут быть полезны для восстановления оригинального текста.

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

Подробнее о книге "MUMPS СУБД"

Комментариев нет:

Отправить комментарий