Мониторинг обменных курсов валют
18c0693f

Сборка системы и ее рабочий цикл


Моделирование Лиспа или другого языка программирования на идеальном базовом Лиспе вполне может послужить иллюстрацией определения операционной семантики языков программирования [8].

Традиционно система программирования для языка Лисп содержит пару интерпретатор-компилятор. Между этими понятиями трудно установить формальную границу. Любой интерпретатор содержит элементы, реализация которых описывается в машинных терминах: структура памяти, реализация двоичных деревьев и т. п. Любая компилированная программа содержит интерпретируемые элементы: например, обращения к файловой системе и другим элементам ОС. На практике достоинства интерпретации проявляются при отладке программ, а преимущества компиляции — при эксплуатации готового программного продукта. Более подробное обсуждение этой темы заслуживает отдельного разговора.

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

Ядро интерпретатора языка Лисп может быть реализовано следующим образом:


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

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

((T . T )(NIL . NIL))

обеспечивает значение T и NIL в соответствии с семантикой базового Лиспа, список

((ОДИН . 1)(ДВА . 2))

дает символьные имена числовым значениям, а список

((ГОЛОВА . CAR)(ХВОСТ . CDR)(УЗЕЛ . CONS))

— синонимы для обозначения базовых операций Лиспа.

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

Основой определения интерпретатора является функция EVAL (evaluation), вычисляющая произвольные выражения языка с учетом состояния ассоциативного списка AL. Спецификация такой функции для базового Лиспа может быть проиллюстрирована следующими примерами:

(EVAL NIL AL ) => NIL (EVAL T AL ) => T (EVAL 'X AL ) => (CADR (ASSOC X AL)) (EVAL '(QOUTE EXPR ) AL ) => EXPR (EVAL '(COND ((T YES) ... )) AL ) => YES (EVAL '(CSETQ X Y EXPR ) AL ) => (EVAL EXPR (CONS '(X Y ) AL)) (EVAL '(CAR A) '((A (1 2 3))(NIL NIL)) ) => 1

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



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

(DEFUN EVAL (e al ) (COND ((MEMBER e '(NIL T )) e ) ((ATOM e ) ((LAMBDA (v ) (COND (v (CADR v ) ) (T (ERROR 'undefdvalue )) )) (ASSOC e al ) ) ) ((EQ (CAR e) 'QUOTE ) (CAR (CDR e )) ) ((EQ (CAR e) 'COND ) (EVCON (CDR e ) al ) ) ((EQ (CAR e) 'LET ) (EVAL (CADDDR e ) (CONS (CONS (CADR e ) (CONS (EVAL (CADDR e ) al ) NIL) ) al ) )) (T (APPLY (CAR e) (EVLIS (CDR e) al ) al ) ) ))

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

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

(DEFUN APPLY (fn args al ) (COND ((MEMBER fn '(CAR CDR CONS ATOM EQ )) (SUBR (CADR (ASSOC fn al)) args )) ((EQ fn NIL) NIL) ((ATOM fn ) (APPLY (EVAL fn al ) args al )) ((EQ (CAR fn ) 'LAMBDA ) (EVAL (CADDR fn ) (APPEND (PAIR (CADR fn ) args ) al ) ) ) ((EQ (CAR fn) 'LABEL ) (APPLY (CADDR fn) args (CONS (CDR fn ) al ) ) ) (T (ERROR- 'undefined_function)) ) )

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

Можно еще поработать с таким определением интерпретатора и более четко локализовать его зависимость от четырех различных категорий объектов: самоопределяемые атомы — (NIL T 1 2 3 ... ), базовые операции над данными языка, обрабатывающие предварительно вычисленные аргументы, — (CAR CDR CONS ATOM EQ ... ), специальные функции, управляющие обработкой аргументов без их предварительного вычисления, — (QUOTE COND LET ...) и конструкторы функций, строящие функциональные значения из обычных выражений, — (LAMBDA LABEL... ).



Желающие могут поэкспериментировать с самодельным интерпретатором, превращая его в модель ядра любого языка программирования, используя какую-нибудь Лисп-систему, например GNU Clisp (с точностью до имен отдельных функций).

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

Ответ.

(DEFINE CIRCLE (al ) (PRINT (EVAL (PRINT (READ )) al )) (CIRCLE al ) )

«Но оно же зациклится!» — скажете вы и будете правы.

Но это не помешает эксперименту, ведь в нашем распоряжении имеется конец файла Ctrl-Z или встроенная операция завершения процесса типа BYE, EXEC, SYSTEM либо что-то подобное.

Упражнение 9.2. Полученные 50 строк определения Лисп-интерпретатора и его вспомогательных функций содержит 1263 символа, если не считать пробелы в начале строк. Попробуйте написать сравнимое определение на каком-нибудь знакомом вам языке программирования.





Самый выгодный курс обмена валюты