(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

LISP-интерпретатор на чистом C

Источник: habrahabr
ababo

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

Уже достаточно долго я размышляю над идеей, объединяющей подходы обоих этих языков. Её суть заключается в реализации языка программирования на основе LISP, решающего те же задачи, что и C: обеспечение высокой степени контроля над оборудованием (включая низкоуровневый доступ к памяти). На практике это будет система LISP-макросов, генерирующая бинарный код. Возможности LISP для препроцессирования исходного кода, как мне кажется, обеспечат небывалую гибкость, в сравнении с препроцессором C или шаблонами C++, при сохранении исходной простоты языка. Это даст возможность на базе такого DSL надстраивать новые расширения, повышающие скорость и удобство разработки. В частности, на этом языке может реализовываться и сама LISP-система.

Написание компилятора требуют наличие кодогенератора, а в конечном итоге - ассемблера. Поэтому практические изыскания стоит начинать с реализации ассемблера (для подмножества инструкций целевого процессора). Мне было интересно минимизировать какие-либо зависимости от конкретных технологий, языков программирования и операционной системы. Поэтому я решил с нуля реализовать на C простейший интерпретатор импровизированного LISP-диалекта, а также написать к нему систему макрорасширений, позволяющих удобно кодировать на подмножестве ассемблера x86. Венцом моих усилий должен стать результирующий загрузочный образ, выводящий "Hello world!" в реальном режиме процессора.

На текущий момент мною реализован работающий интерпретатор (файл int.c, около 900 строк C-кода), а также набор базовых функций и макросов (файл lib.l, около 100 строк LISP-кода). Кому интересны принципы выполнения LISP-кода, а также подробности реализации интерпретатора, прошу под кат.

Базовой единицей LISP-вычислений является точечная пара (dotted pair). В классическом лиспе Маккарти точечная пара и символ - единственные два типа данных. В практических реализациях этот набор приходится расширять, как минимум, числами. Кроме того, к базовым типам также добавляют строки и массивы (первые являются разновидностью вторых). В стремлении к упрощению есть искушение рассматривать строки в качестве списка чисел, но я сознательно отказался от этой идеи, как от резко ограничивающей возможности языка в реальном мире. В качестве контейнера для чисел решил использовать double. 

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

struct l_env;

typedef struct s_expr *(*built_in) (struct s_expr*, struct l_env*,
                                    struct file_pos*);

struct s_expr {
  enum {
    DOTTED_PAIR, STRING, SYMBOL, NUMBER, FUNCTION, MACRO,
    BUILT_IN_FUNCTION, BUILT_IN_MACRO
  } type;
  union {
    struct {
      struct s_expr *first, *rest;
    } pair;
    struct {
      char *ptr;
      size_t size;
    } string;
    struct {
      struct s_expr *expr;
      struct l_env *env;
    } function;
    char *symbol;
    double number;
    built_in built_in;
  } u;
};

struct l_env {
  char *symbol;
  struct s_expr *expr;
  struct l_env *next;
};

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

Как видно из вышеприведённого кода, функция (и макрос) ссылаются на структуру l_env. Она является базовым элементом лексического окружения, хранимого в виде списка. Конечно, это неэффективно, поскольку предполагает последовательный доступ к символам. Зато это очень простая и удобная структура для поддержки локальных переменных: они добавляются в голову списка, когда как глобальные - в хвост. От локальных переменных очень легко избавляться (при выходе из функции или из блока let), просто игнорируя переднюю часть этого списка. Собственное лексическое окружение у функции позволяет реализовывать замыкания.

На базе вышеприведённой структуры s-выражения легко построить функцию его вычисления:

struct s_expr *eval_s_expr (struct s_expr *expr, struct l_env *env,
                            struct file_pos *pos) {
  struct s_expr *first, *in = expr;
  struct l_env *benv;
 
  trace_put("%s -> ...", in, NULL, env);
 
  if (expr)
    if (expr->type == SYMBOL)
      if (find_symbol(expr->u.symbol, &env))
        expr = env->expr;
      else
        error(UNBOUND_SYMBOL_MSG, pos, expr->u.symbol);
    else if (expr->type == DOTTED_PAIR) {
      first = eval_s_expr(expr->u.pair.first, env, pos);
 
      if (!first // first->type == DOTTED_PAIR // first->type == SYMBOL //
          first->type == STRING // first->type == NUMBER)
        error(NON_FUNC_MACRO_MSG, pos, s_expr_string(first, env));
 
      expr = first->type == FUNCTION // first->type == BUILT_IN_FUNCTION ?
        map_eval(expr->u.pair.rest, env, pos) : expr->u.pair.rest;
 
      if (first->type == FUNCTION // first->type == MACRO) {
        assert(first->u.function.expr->type == DOTTED_PAIR);
 
        benv = apply_args(first->u.function.expr->u.pair.first, expr,
                          first->u.function.env, pos);
 
        expr = eval_list(first->u.function.expr->u.pair.rest, benv, pos);
 
        if (first->type == MACRO) {
          trace_put("%s ~> %s", in, expr, env);
          expr = eval_s_expr(expr, env, pos);
        }
      }
      else
        expr = first->u.built_in(expr, env, pos);
    }
 
  trace_put("%s -> %s", in, expr, env);
 
  return expr;
}

Если вычислимое выражение является символом, мы просто ищем его значение в текущем лексическом окружении (find_symbol). Если вызов функции: вначале вычисляем фактические параметры, используя текущее лексическое окружение (map_eval), затем привязываем их к символам формальных параметров (apply_args) уже в лексическом окружении самой функции. Далее последовательно вычисляем элементы тела на основе полученного лексического окружения, возвращая значение последнего выражения (eval_list). Для вызова макроса порядок вычисления несколько иной. Фактические параметры не вычисляются, а передаются в неизменном виде. Кроме того, результирующее выражение макроса (макроподстановка) подвергается дополнительному вычислению. Числа, строки, функции и макросы вычисляются сами в себя.

Полный текст файла int.c

Я решил ввести более лаконичные названия для базовых и произвольных функций и макросов. В классическом LISP (и, особенно, в Common Lisp) меня немного напрягает многословность базовых примитивов. С одной стороны, я не хотел усложнять парсер, потому quote и backquote синтаксис им не поддерживается, только скобочная нотация. С другой стороны, стремился компенсировать избыточную скобочность широким использованием специальных символов для лаконичности. Кому-то это покажется весьма спорным решением.

Имена я старался подбирать в соответствии с их ассоциативным рядом:

  • _ - заменяет nil
  • ! - заменяет lambda
  • # - аналогично !, но объявляет безымянный макрос
  • ? - заменяет if с обязательным третим параметром
  • ^ - заменяет cons
  • @ - заменяет car
  • % - заменяет cdr
  • = - заменяет setq

Соответственно, имена производных функций и макросов во многом стали производными от имён базовых:
  • !! - заменяет defun
  • ## - заменяет defmacro
  • ^^ - заменяет list
  • @% - заменяет cadr
  • %% - заменяет cddr
  • : - заменяет let для одной переменной
  • :: - заменяет let без избыточных скобок
  • & - заменяет and
  • / - заменяет or

Теперь рассмотрим производные определения. Вначале определим базовые сокращения:

(= @% (! (list) (@ (% list)))) ; cadr
(= %% (! (list) (% (% list)))) ; cddr
(= ^^ (! (_ . elts) elts)) ; list

(= ## (# (name fargs . body) ; defmacro
         (^^ = name (^ # (^ fargs body)))))
(## !! (name fargs . body) ; defun
       (^^ = name (^ ! (^ fargs body))))

Обратите внимание на точечную нотацию списка формальных аргументов. Символ после точки захватывает оставшиеся фактические параметры. Случай, когда все аргументы необязательны, описывается специальной нотацией: (_ . rest-args). Далее определим классический map и два парных разбиения списка:

(!! map (func list)
    (? list (^ (func (@ list)) (map func (% list))) _))

(!! pairs1 (list) ; (a b c d) -> ((a b) (b c) (c d))
    (? (% list) (^ (^^ (@ list) (@% list)) (pairs1 (% list))) _))
(!! pairs2 (list) ; (a b c d) -> ((a b) (c d))
    (? list (^ (^^ (@ list) (@% list)) (pairs2 (%% list))) _))

Определяем два варианта let:

(## : (name value . body) ; simplified let
    (^^ (^ ! (^ (^^ name) body)) value))
(## :: (vars . body) ; let without redundant braces
    (= vars (pairs2 vars))
    (^ (^ ! (^ (map @ vars) body)) (map @% vars)))

Классический reverse и левую свёртку:
(!! reverse (list)
    (: reverse+ _
       (!! reverse+ (list rlist)
           (? list (reverse+ (% list) (^ (@ list) rlist)) rlist))
       (reverse+ list _)))

(!! fold (list func last) ; (fold (' (a b)) f l) <=> (f a (f b l))
    (? list (func (@ list) (fold (% list) func last)) last))

Теперь логические операторы на основе if:
(= t (' t)) ; true constant
(!! ~ (bool) (? bool _ t)) ; not
(## & (_ . bools) ; and
    (: and (! (bool1 bool2) (^^ ? bool1 (^^ ? bool2 t _) _))
       (fold bools and t)))
(## / (_ . bools) ; or
    (: or (! (bool1 bool2) (^^ ? bool1 t (^^ ? bool2 t _)))
       (fold bools or _)))

И, наконец, операторы сравнения на основе встроенного > (greater):
(: defcmp (! (cmp)
             (# (_ . nums)
                (: cmp+ (! (pair bool)
                           (^^ & (cmp (@ pair) (@% pair)) bool))
                   (fold (pairs1 nums) cmp+ t))))
   (= == (defcmp (! (num1 num2) (^^ & (^^ ~ (^^ > num1 num2))
                                      (^^ ~ (^^ > num2 num1))))))
   (= >= (defcmp (! (num1 num2) (^^ ~ (^^ > num2 num1))))))
(## < (_ . nums) (^ > (reverse nums)))
(## <= (_ . nums) (^ >= (reverse nums)))

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

Полный тест файла lib.l
;/
Formal argument list notation:
  ([{arg1 [arg2 [arg3 ...]] / _} [. args]])

Number notation:
  ${double / ooctal / hhex} ; $4 $-2.2e3 $o376 $h7EF

Built-in symbols:
  _ ; nil

Built-in functions:
  @ (list) ; car
  % (list) ; cdr
  ^ (first rest) ; cons
  + (_ . nums)

Built-in macros:
  trace (_ . body)
  ' (expr)
  ? (cond texpr fexpr) ; if with mandatory fexpr
  ! (args . body) ; lambda
  # (args . body) ; creates anonymous macro
  > (_ . nums)
/;

(= @% (! (list) (@ (% list)))) ; cadr
(= %% (! (list) (% (% list)))) ; cddr
(= ^^ (! (_ . elts) elts)) ; list

(= ## (# (name fargs . body) ; defmacro
         (^^ = name (^ # (^ fargs body)))))
(## !! (name fargs . body) ; defun
       (^^ = name (^ ! (^ fargs body))))

(!! map (func list)
    (? list (^ (func (@ list)) (map func (% list))) _))

(!! pairs1 (list) ; (a b c d) -> ((a b) (b c) (c d))
    (? (% list) (^ (^^ (@ list) (@% list)) (pairs1 (% list))) _))
(!! pairs2 (list) ; (a b c d) -> ((a b) (c d))
    (? list (^ (^^ (@ list) (@% list)) (pairs2 (%% list))) _))

(## : (name value . body) ; simplified let
    (^^ (^ ! (^ (^^ name) body)) value))
(## :: (vars . body) ; let without redundant braces
    (= vars (pairs2 vars))
    (^ (^ ! (^ (map @ vars) body)) (map @% vars)))

(!! reverse (list)
    (: reverse+ _
       (!! reverse+ (list rlist)
           (? list (reverse+ (% list) (^ (@ list) rlist)) rlist))
       (reverse+ list _)))

(!! fold (list func last) ; (fold (' (a b)) f l) <=> (f a (f b l))
    (? list (func (@ list) (fold (% list) func last)) last))

(= t (' t)) ; true constant
(!! ~ (bool) (? bool _ t)) ; not
(## & (_ . bools) ; and
    (: and (! (bool1 bool2) (^^ ? bool1 (^^ ? bool2 t _) _))
       (fold bools and t)))
(## / (_ . bools) ; or
    (: or (! (bool1 bool2) (^^ ? bool1 t (^^ ? bool2 t _)))
       (fold bools or _)))

(: defcmp (! (cmp)
             (# (_ . nums)
                (: cmp+ (! (pair bool)
                           (^^ & (cmp (@ pair) (@% pair)) bool))
                   (fold (pairs1 nums) cmp+ t))))
   (= == (defcmp (! (num1 num2) (^^ & (^^ ~ (^^ > num1 num2))
                                      (^^ ~ (^^ > num2 num1))))))
   (= >= (defcmp (! (num1 num2) (^^ ~ (^^ > num2 num1))))))
(## < (_ . nums) (^ > (reverse nums)))
(## <= (_ . nums) (^ >= (reverse nums)))


Итак, интерпретатор и большая часть примитивов готовы для того, чтобы писать DSL ассемблера. Буду пробовать…

Ссылки по теме


 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 20.09.2012 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Microsoft Office 365 Профессиональный Плюс. Подписка на 1 рабочее место на 1 год
Microsoft Office 365 Бизнес. Подписка на 1 рабочее место на 1 год
Microsoft Windows Professional 10, Электронный ключ
Microsoft 365 Business Basic (corporate)
Microsoft 365 Business Standard (corporate)
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Безопасность компьютерных сетей и защита информации
Новости ITShop.ru - ПО, книги, документация, курсы обучения
Программирование на Microsoft Access
CASE-технологии
Реестр Windows. Секреты работы на компьютере
Программирование на Visual С++
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100