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

в 11:48, , рубрики: c++, интерпретатор, ненормальное программирование, метки: ,

Я люблю язык 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-выражения:

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

#include <assert.h>
#include <ctype.h>
#include <float.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define LINE_COMMENT_CHAR ';'
#define BLOCK_COMMENT_CHAR1 ';'
#define BLOCK_COMMENT_CHAR2 '|'
#define LIST_OPEN_BRACE_CHAR '('
#define LIST_CLOSE_BRACE_CHAR ')'
#define LIST_DOT_CHAR '.'
#define STRING_DELIMITER_CHAR '"'
#define STRING_ESCAPE_CHAR '\'
#define NUMBER_PREFIX_CHAR '$'
#define NUMBER_FORMAT_HEX_CHAR 'h'
#define NUMBER_FORMAT_OCT_CHAR 'o'

#define NIL_SYMBOL_STR "_"
#define TRUE_SYMBOL_STR "t"
#define TRACE_SYMBOL_STR "trace"
#define CAR_SYMBOL_STR "@"
#define CDR_SYMBOL_STR "%"
#define CONS_SYMBOL_STR "^"
#define IF_SYMBOL_STR "?"
#define LAMBDA_SYMBOL_STR "!"
#define MACRO_SYMBOL_STR "#"
#define SETQ_SYMBOL_STR "="
#define QUOTE_SYMBOL_STR "'"
#define PLUS_SYMBOL_STR "+"
#define GREATER_SYMBOL_STR ">"

#define FUNCTION_STR_FORMAT "<!%s>"
#define MACRO_STR_FORMAT "<#%s>"

#define OUT_OF_MEMORY_MSG "out of memory"
#define UNEXPECTED_EOF_MSG "unexpected end of file"
#define BAD_SYNTAX_MSG "bad syntax"
#define NON_FUNC_MACRO_MSG "expression %s is neither a function nor a macro"
#define NON_NONEMPTY_LIST_MSG "expression %s is not a nonempty list"
#define NON_LIST_MSG "expression %s is not a proper list"
#define UNBOUND_SYMBOL_MSG "unbound symbol %s"
#define BAD_FORMAL_ARGS_MSG "bad formal arguments %s"
#define BAD_ACTUAL_ARGS_MSG "bad actual arguments %s"
#define STRING_OVERFLOW_MSG "string size overflow"

#define NUMBER_LENGTH_MAX 32
#define SYMBOL_LENGTH_MAX 32
#define STRING_LENGTH_MAX 256
#define S_EXPR_LENGTH_MAX 1024

struct file_pos {
  char *filename;
  int line, chr;
};

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;
};

void error(char *message, struct file_pos *pos, char *expr) {
  if (pos)
    printf("Error at %s:%d:%d: ", pos->filename, pos->line, pos->chr);
  else
    printf("Error: ");
  if (expr)
    printf(message, expr);
  else
    printf("%s", message);
  puts("");
  exit(1);
}

void *alloc_mem(size_t size) {
  void *ptr = malloc(size);
  if (!ptr)
    error(OUT_OF_MEMORY_MSG, NULL, NULL);
  return ptr;
}

struct s_expr *true_ () {
  static struct s_expr *expr = NULL;
  if (!expr) {
    expr = alloc_mem(sizeof(*expr));
    expr->type = SYMBOL;
    expr->u.symbol = TRUE_SYMBOL_STR;
  }
  return expr;
}

int get_char(FILE *file, struct file_pos *pos) {
  int chr = getc(file);
  if (chr == 'n')
    pos->line++, pos->chr = 1;
  else if (chr != EOF)
    pos->chr++;
  return chr;
}

int next_char(FILE *file) {
  int chr = getc(file);
  ungetc(chr, file);
  return chr;
}

int get_significant_char (FILE *file, struct file_pos *pos) {
  enum { NO_COMMENT, LINE_COMMENT, BLOCK_COMMENT } state = NO_COMMENT;
  int chr;

  while (1) {
    chr = get_char(file, pos);
    if (state == NO_COMMENT) {
      if (chr == BLOCK_COMMENT_CHAR1 &&
          next_char(file) == BLOCK_COMMENT_CHAR2) {
        get_char(file, pos);
        state = BLOCK_COMMENT;
        continue;
      }
      if (chr == LINE_COMMENT_CHAR)
        state = LINE_COMMENT;
      else if (chr != ' ' && chr != 't' && chr != 'r' && chr != 'n')
        return chr;
    }
    else if (state == BLOCK_COMMENT) {
      if (chr == BLOCK_COMMENT_CHAR2 &&
          next_char(file) == BLOCK_COMMENT_CHAR1) {
        get_char(file, pos);
        state = NO_COMMENT;
      }
      else if (chr == EOF)
        error(UNEXPECTED_EOF_MSG, pos, NULL);
    }
    else if (state == LINE_COMMENT) {
      if (chr == 'n')
        state = NO_COMMENT;
      else if (chr == EOF)
        return EOF;
    }
  }
}

struct s_expr *parse_s_expr (FILE*, struct file_pos*);

struct s_expr *parse_list (FILE *file, struct file_pos *pos) {
  struct s_expr *expr, *rest;
  int chr;

  chr = get_significant_char(file, pos);
  if (chr == LIST_CLOSE_BRACE_CHAR)
    return NULL;

  ungetc(chr, file);
  pos->chr--;
  expr = alloc_mem(sizeof(*expr));
  expr->type = DOTTED_PAIR;
  expr->u.pair.first = parse_s_expr(file, pos);
  rest = expr;

  while (1) {
    chr = get_significant_char(file, pos);
    if (chr == LIST_DOT_CHAR) {
      rest->u.pair.rest = parse_s_expr(file, pos);
      if (get_significant_char(file, pos) != LIST_CLOSE_BRACE_CHAR)
        error(BAD_SYNTAX_MSG, pos, NULL);
      break;
    }
    else if (chr == LIST_CLOSE_BRACE_CHAR) {
      rest->u.pair.rest = NULL;
      break;
    }
    else if (chr == EOF)
      error(UNEXPECTED_EOF_MSG, pos, NULL);
    else {
      ungetc(chr, file);
      pos->chr--;
      rest->u.pair.rest = alloc_mem(sizeof(*expr));
      rest->u.pair.rest->type = DOTTED_PAIR;
      rest->u.pair.rest->u.pair.first = parse_s_expr(file, pos);
      rest = rest->u.pair.rest;
    }
  }

  return expr;
}

void read_escape_seq (FILE *file, struct file_pos *pos, char *buf) {
/* TODO: add support for escape sequences */

}

struct s_expr *parse_string (FILE *file, struct file_pos *pos) {
  char buf[STRING_LENGTH_MAX];
  struct s_expr *expr;
  int chr, i = 0;

  while (i < STRING_LENGTH_MAX) {
    chr = get_char(file, pos);
    if (chr == STRING_ESCAPE_CHAR)
      read_escape_seq(file, pos, buf);
    else if (chr == STRING_DELIMITER_CHAR)
      break;
    else if (chr == EOF)
      error(UNEXPECTED_EOF_MSG, pos, NULL);
    else
      buf[i++] = chr;
  }

  expr = alloc_mem(sizeof(*expr));
  expr->type = STRING;
  expr->u.string.ptr = i ? alloc_mem(i) : NULL;
  memcpy(expr->u.string.ptr, buf, i);
  expr->u.string.size = i;

  return expr;
}

void read_double (FILE *file, struct file_pos *pos, char *buf) {
  int chr, i = 0, point = -1;

  chr = next_char(file);
  if (chr == '+' || chr == '-') {
    get_char(file, pos);
    buf[i++] = chr;
  }

  while (i < NUMBER_LENGTH_MAX && isdigit(next_char(file)))
    buf[i++] = get_char(file, pos);

  if (i < NUMBER_LENGTH_MAX && next_char(file) == '.')
    buf[point = i++] = get_char(file, pos);

  while (i < NUMBER_LENGTH_MAX && isdigit(next_char(file)))
    buf[i++] = get_char(file, pos);

  chr = next_char(file);
  if (i < NUMBER_LENGTH_MAX && (chr == 'e' || chr == 'E') && i > point + 1) {
    get_char(file, pos);
    buf[i++] = chr;

    chr = next_char(file);
    if (i < NUMBER_LENGTH_MAX && (chr == '+' || chr == '-')) {
      get_char(file, pos);
      buf[i++] = chr;
    }

    while (i < NUMBER_LENGTH_MAX && isdigit(next_char(file)))
      buf[i++] = get_char(file, pos);
  }

  if (i && i < NUMBER_LENGTH_MAX)
    buf[i] = 0;
  else
    error(BAD_SYNTAX_MSG, pos, NULL);
}

void read_int (FILE *file, struct file_pos *pos, int base, char *buf) {
  int chr, i = 0;

  assert(base == 8 || base == 16);

  for (; i < NUMBER_LENGTH_MAX; get_char(file, pos)) {
    chr = next_char(file);
    if ((base == 16 && isxdigit(chr)) || (chr >= '0' && chr <= '7'))
      buf[i++] = chr;
    else
      break;
  }

  if (i && i < NUMBER_LENGTH_MAX)
    buf[i] = 0;
  else
    error(BAD_SYNTAX_MSG, pos, NULL);
}

struct s_expr *parse_number (FILE *file, struct file_pos *pos) {
  char buf[NUMBER_LENGTH_MAX + 1];
  struct s_expr *expr;
  int inum;

  expr = alloc_mem(sizeof(*expr));
  expr->type = NUMBER;

  switch (next_char(file)) {
  case NUMBER_FORMAT_HEX_CHAR:
    get_char(file, pos);
    read_int(file, pos, 16, buf);
    sscanf(buf, "%x", &inum);
    expr->u.number = inum;
    break;
  case NUMBER_FORMAT_OCT_CHAR:
    get_char(file, pos);
    read_int(file, pos, 8, buf);
    sscanf(buf, "%o", &inum);
    expr->u.number = inum;
    break;
  default:
    read_double(file, pos, buf);
    sscanf(buf, "%lf", &expr->u.number);
    break;
  }

  return expr;
}

struct s_expr *parse_symbol (FILE *file, struct file_pos *pos) {
  char buf[NUMBER_LENGTH_MAX + 1];
  struct s_expr *expr;
  int chr, chr2, i = 0;

  for (; i < NUMBER_LENGTH_MAX; get_char(file, pos)) {
    chr = next_char(file);
    if (chr == BLOCK_COMMENT_CHAR1) {
      get_char(file, pos);
      chr2 = next_char(file);
      ungetc(chr2, file);
      pos->chr--;

      if (chr2 == BLOCK_COMMENT_CHAR2)
        break;
    }
    if (chr >= '!' && chr <= '~' && chr != LINE_COMMENT_CHAR &&
        chr != LIST_OPEN_BRACE_CHAR && chr != LIST_CLOSE_BRACE_CHAR &&
        chr != LIST_DOT_CHAR && chr != STRING_DELIMITER_CHAR &&
        chr != NUMBER_PREFIX_CHAR)
      buf[i++] = chr;
    else
      break;
  }

  if (i && i < SYMBOL_LENGTH_MAX)
    buf[i] = 0;
  else
    error(BAD_SYNTAX_MSG, pos, NULL);

  if(!strcmp(buf, NIL_SYMBOL_STR))
    return NULL;
  if(!strcmp(buf, TRUE_SYMBOL_STR))
    return true_();

  expr = alloc_mem(sizeof(*expr));
  expr->type = SYMBOL;
  expr->u.symbol = alloc_mem(i + 1);
  strcpy(expr->u.symbol, buf);

  return expr;
}

struct s_expr *parse_s_expr (FILE *file, struct file_pos *pos) {
  struct s_expr *expr;
  int chr;

  chr = get_significant_char(file, pos);

  switch (chr) {
  case EOF:
    return NULL;
  case LIST_OPEN_BRACE_CHAR:
    expr = parse_list(file, pos);
    break;
  case STRING_DELIMITER_CHAR:
    expr = parse_string(file, pos);
    break;
  case NUMBER_PREFIX_CHAR:
    expr = parse_number(file, pos);
    break;
  default:
    ungetc(chr, file);
    pos->chr--;
    expr = parse_symbol(file, pos);
    break;
  }

  return expr;
}

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

static int do_trace = 0;

char *s_expr_string (struct s_expr*, struct l_env*);

void trace_put (char *format, struct s_expr *expr1, struct s_expr *expr2,
                struct l_env *env) {
  if (do_trace) {
    printf("Trace: ");
    printf(format, s_expr_string(expr1, env), s_expr_string(expr2, env));
    puts("");
  }
}

struct l_env *add_symbol (char *symbol, struct s_expr *expr,
                          struct l_env *env, int append) {
  struct l_env *new_env;
  new_env = alloc_mem(sizeof(*new_env));
  new_env->symbol = symbol, new_env->expr = expr;
  if (append)
    env->next = new_env, new_env->next = NULL;
  else
    new_env->next = env;
  return new_env;
}

struct l_env * add_built_in (int macro, char *symbol, built_in bi,
                             struct l_env *env) {
  struct s_expr *expr = alloc_mem(sizeof(*expr));
  expr->type = macro ? BUILT_IN_MACRO : BUILT_IN_FUNCTION;
  expr->u.built_in = bi;
  return add_symbol(symbol, expr, env, 0);
}

int find_symbol (char *symbol, struct l_env **env) {
  struct l_env *next = *env;
  for (; next; *env = next, next = next->next)
    if (!strcmp(symbol, next->symbol)) {
      *env = next;
      return 1;
    }
  return 0;
}

char *str_cat (char *dest, size_t dest_size, char *src) {
  if (strlen(src) > dest_size - 1 - strlen(dest))
    error(STRING_OVERFLOW_MSG, NULL, NULL);
  return strcat(dest, src);
}

char *list_string (struct s_expr *list, struct l_env *env) {
  char buf[S_EXPR_LENGTH_MAX + 1] = { LIST_OPEN_BRACE_CHAR, 0 };
  char psep[] = { ' ', LIST_DOT_CHAR, ' ', 0 };
  char cbrc[] = { LIST_CLOSE_BRACE_CHAR, 0 };

  for (; list && list->type == DOTTED_PAIR; list = list->u.pair.rest) {
    if (buf[1])
      str_cat(buf, S_EXPR_LENGTH_MAX + 1, " ");
    str_cat(buf, S_EXPR_LENGTH_MAX + 1,
            s_expr_string(list->u.pair.first, env));
  }

  if (list)
    str_cat(str_cat(buf, S_EXPR_LENGTH_MAX + 1, psep),
            S_EXPR_LENGTH_MAX + 1, s_expr_string(list, env));

  str_cat(buf, S_EXPR_LENGTH_MAX + 1, cbrc);

  return strcpy(alloc_mem(strlen(buf) + 1), buf);
}

char *string_string (char *ptr, size_t size) {
  char *str = alloc_mem(size + 3);
  str[0] = str[size + 1] = '"';
  memcpy(str + 1, ptr, size);
  str[size + 2] = 0;
  return str;
}

char *number_string (double number) {
  char *str = alloc_mem(NUMBER_LENGTH_MAX + 2);
  str[0] = NUMBER_PREFIX_CHAR;
  sprintf(str + 1, "%g", number);
  return str;
}

char *function_string (struct s_expr *expr, int macro, struct l_env *env) {
  char *str;

  for (; env; env = env->next)
    if (env->expr == expr)
      break;

  str = alloc_mem((macro ? sizeof(MACRO_STR_FORMAT) :
                   sizeof(FUNCTION_STR_FORMAT)) +
                  (env ? strlen(env->symbol) : 0) - 1);

  sprintf(str, macro ? MACRO_STR_FORMAT : FUNCTION_STR_FORMAT,
          env ? env->symbol : "");

  return str;
}

char *s_expr_string (struct s_expr *expr, struct l_env *env) {
  if (!expr)
    return NIL_SYMBOL_STR;

  switch (expr->type) {
  case DOTTED_PAIR:
    return list_string(expr, env);
  case STRING:
    return string_string(expr->u.string.ptr, expr->u.string.size);
  case SYMBOL:
    return expr->u.symbol;
  case NUMBER:
    return number_string(expr->u.number);
  case FUNCTION:
  case BUILT_IN_FUNCTION:
    return function_string(expr, 0, env);
  case MACRO:
  case BUILT_IN_MACRO:
    return function_string(expr, 1, env);
  default:
    assert(0);
    return NULL;
  }
}

int proper_listp (struct s_expr *expr) {
  while (expr && expr->type == DOTTED_PAIR)
    expr = expr->u.pair.rest;
  return expr == NULL;
}

struct s_expr *search_symbol(struct s_expr *list, char *symbol) {
  for (; list && list->type == DOTTED_PAIR; list = list->u.pair.rest) {
    assert(list->u.pair.first->type == SYMBOL);
    if (!strcmp(list->u.pair.first->u.symbol, symbol))
      return list;
  }
  return NULL;
}

void check_fargs (struct s_expr *fargs, struct l_env *env,
                  struct file_pos *pos) {
  struct s_expr *rest = fargs;

  if (rest && rest->type == DOTTED_PAIR &&
      !rest->u.pair.first && rest->u.pair.rest->type == SYMBOL)
    return;

  for (; rest && rest->type == DOTTED_PAIR; rest = rest->u.pair.rest)
    if (!rest->u.pair.first || rest->u.pair.first->type != SYMBOL ||
        search_symbol(fargs, rest->u.pair.first->u.symbol) != rest)
      error(BAD_FORMAL_ARGS_MSG, pos, s_expr_string(fargs, env));

  if (rest && (rest->type != SYMBOL || search_symbol(fargs, rest->u.symbol)))
    error(BAD_FORMAL_ARGS_MSG, pos, s_expr_string(fargs, env));
}

void check_aargs (struct s_expr *args, int count, int va, struct l_env *env,
                  struct file_pos *pos) {
  struct s_expr *rest = args;

  for (; count && rest && rest->type == DOTTED_PAIR; count--)
    rest = rest->u.pair.rest;

  if (count || (!va && rest) || !proper_listp(rest))
    error(BAD_ACTUAL_ARGS_MSG, pos, s_expr_string(args, env));
}

struct s_expr *eval_list (struct s_expr*, struct l_env*, struct file_pos*);
struct s_expr *eval_s_expr (struct s_expr*, struct l_env*, struct file_pos*);

#define ARG1(args) args->u.pair.first
#define ARG2(args) args->u.pair.rest->u.pair.first
#define ARG3(args) args->u.pair.rest->u.pair.rest->u.pair.first

struct s_expr *trace (struct s_expr *args, struct l_env *env,
                      struct file_pos *pos) {
  struct s_expr *expr;
  do_trace = 1;
  expr = eval_list(args, env, pos);
  do_trace = 0;
  return expr;
}

struct s_expr *quote (struct s_expr *args, struct l_env *env,
                      struct file_pos *pos) {
  check_aargs(args, 1, 0, env, pos);
  return ARG1(args);
}

struct s_expr *car (struct s_expr *args, struct l_env *env,
                    struct file_pos *pos) {
  check_aargs(args, 1, 0, env, pos);
  if (ARG1(args) && ARG1(args)->type != DOTTED_PAIR)
    error(NON_LIST_MSG, pos, s_expr_string(ARG1(args), env));
  return ARG1(args) ? ARG1(args)->u.pair.first : NULL;
}

struct s_expr *cdr (struct s_expr *args, struct l_env *env,
                    struct file_pos *pos) {
  check_aargs(args, 1, 0, env, pos);
  if (ARG1(args) && ARG1(args)->type != DOTTED_PAIR)
    error(NON_LIST_MSG, pos, s_expr_string(ARG1(args), env));
  return ARG1(args) ? ARG1(args)->u.pair.rest : NULL;
}

struct s_expr *cons (struct s_expr *args, struct l_env *env,
                     struct file_pos *pos) {
  struct s_expr *expr;
  check_aargs(args, 2, 0, env, pos);
  expr = alloc_mem(sizeof(*expr));
  expr->type = DOTTED_PAIR;
  expr->u.pair.first = ARG1(args);
  expr->u.pair.rest = ARG2(args);
  return expr;
}

struct s_expr *if_ (struct s_expr *args, struct l_env *env,
                    struct file_pos *pos) {
  check_aargs(args, 3, 0, env, pos);
  return eval_s_expr(ARG1(args), env, pos) ?
    eval_s_expr(ARG2(args), env, pos) :
    eval_s_expr(ARG3(args), env, pos);
}

struct s_expr *function (struct s_expr *args, struct l_env *env,
                         struct file_pos *pos, int macro) {
  struct s_expr *expr;
  check_aargs(args, 1, 1, env, pos);
  check_fargs(ARG1(args), env, pos);
  expr = alloc_mem(sizeof(*expr));
  expr->type = macro ? MACRO : FUNCTION;
  expr->u.function.expr = args;
  expr->u.function.env = env;
  return expr;
}

struct s_expr *lambda (struct s_expr *args, struct l_env *env,
                       struct file_pos *pos) {
  return function(args, env, pos, 0);
}

struct s_expr *macro (struct s_expr *args, struct l_env *env,
                      struct file_pos *pos) {
  return function(args, env, pos, 1);
}

struct s_expr *setq (struct s_expr *args, struct l_env *env,
                     struct file_pos *pos) {
  struct s_expr *rest = args, *expr = NULL;
  struct l_env *senv;

  while (rest && rest->type == DOTTED_PAIR) {
    if (ARG1(rest) && ARG1(rest)->type == SYMBOL &&
        rest->u.pair.rest && rest->u.pair.rest->type == DOTTED_PAIR) {
      expr = eval_s_expr(ARG2(rest), env, pos), senv = env;
      if (find_symbol(ARG1(rest)->u.symbol, &senv)) {
        trace_put("%s => %s [assign]", expr, ARG1(rest), env);
        senv->expr = expr;
      }
      else {
        trace_put("%s => %s [global]", expr, ARG1(rest), env);
        add_symbol(ARG1(rest)->u.symbol, expr, senv, 1);
      }
    }
    else
      error(BAD_ACTUAL_ARGS_MSG, pos, s_expr_string(args, env));

    rest = rest->u.pair.rest->u.pair.rest;
  }

  if (rest)
    error(BAD_ACTUAL_ARGS_MSG, pos, s_expr_string(args, env));

  return expr;
}

struct s_expr *plus (struct s_expr *args, struct l_env *env,
                     struct file_pos *pos) {
  struct s_expr *rest = args;
  double sum = 0;

  while (rest && rest->type == DOTTED_PAIR && ARG1(rest)->type == NUMBER)
    sum += ARG1(rest)->u.number, rest = rest->u.pair.rest;

  if (rest)
    error(BAD_ACTUAL_ARGS_MSG, pos, s_expr_string(args, env));

  rest = alloc_mem(sizeof(*rest));
  rest->type = NUMBER;
  rest->u.number = sum;
  return rest;
}

struct s_expr *greater (struct s_expr *args, struct l_env *env,
                        struct file_pos *pos) {
  struct s_expr *rest = args, *num;
  double prev = DBL_MAX;

  while (rest && rest->type == DOTTED_PAIR) {
    num = eval_s_expr(ARG1(rest), env, pos);

    if (!num || num->type != NUMBER)
      error(BAD_ACTUAL_ARGS_MSG, pos, s_expr_string(args, env));

    if (prev - num->u.number < DBL_EPSILON)
      return NULL;

    prev = num->u.number, rest = rest->u.pair.rest;
  }

  if (rest)
    error(BAD_ACTUAL_ARGS_MSG, pos, s_expr_string(args, env));

  return true_();
}

struct l_env *create_env () {
  struct l_env *env = NULL;
  env = add_built_in(1, TRACE_SYMBOL_STR, trace, env);
  env = add_built_in(1, QUOTE_SYMBOL_STR, quote, env);
  env = add_built_in(0, CAR_SYMBOL_STR, car, env);
  env = add_built_in(0, CDR_SYMBOL_STR, cdr, env);
  env = add_built_in(0, CONS_SYMBOL_STR, cons, env);
  env = add_built_in(1, IF_SYMBOL_STR, if_, env);
  env = add_built_in(1, LAMBDA_SYMBOL_STR, lambda, env);
  env = add_built_in(1, MACRO_SYMBOL_STR, macro, env);
  env = add_built_in(1, SETQ_SYMBOL_STR, setq, env);
  env = add_built_in(0, PLUS_SYMBOL_STR, plus, env);
  env = add_built_in(1, GREATER_SYMBOL_STR, greater, env);
  return env;
}

struct s_expr *map_eval (struct s_expr *list, struct l_env *env,
                         struct file_pos *pos) {
  struct s_expr *expr = NULL, *rest;
    
  while (list) {
    if (list->type != DOTTED_PAIR)
      error(NON_LIST_MSG, pos, s_expr_string(list, env));
	if (expr) {
      rest->u.pair.rest = alloc_mem(sizeof(*expr));
      rest = rest->u.pair.rest;
    }
    else
      expr = rest = alloc_mem(sizeof(*expr));
    rest->type = DOTTED_PAIR;
    rest->u.pair.first = eval_s_expr(list->u.pair.first, env, pos);
    list = list->u.pair.rest;
  }

  if (expr)
    rest->u.pair.rest = NULL;

  return expr;
}

struct l_env *apply_args (struct s_expr *fargs, struct s_expr *aargs,
                          struct l_env *env, struct file_pos *pos) {
  struct s_expr *rest = aargs;

  if (!fargs || fargs->u.pair.first)
    while (fargs && fargs->type == DOTTED_PAIR) {
      if (!rest || rest->type != DOTTED_PAIR)
        error(BAD_ACTUAL_ARGS_MSG, pos, s_expr_string(aargs, env));
      assert(fargs->u.pair.first->type == SYMBOL);
      trace_put("%s => %s [local]", rest->u.pair.first,
                fargs->u.pair.first, env);
      env = add_symbol(fargs->u.pair.first->u.symbol,
                       rest->u.pair.first, env, 0);
      fargs = fargs->u.pair.rest, rest = rest->u.pair.rest;
    }
  else
    fargs = fargs->u.pair.rest;

  if (fargs) {
    assert(fargs->type == SYMBOL);
    if (rest && !proper_listp(rest))
      error(BAD_ACTUAL_ARGS_MSG, pos, s_expr_string(aargs, env));
    trace_put("%s => %s [local]", rest, fargs, env);
    env = add_symbol(fargs->u.symbol, rest, env, 0);
  }
  else if (rest)
    error(BAD_ACTUAL_ARGS_MSG, pos, s_expr_string(aargs, env));

  return env;
}

struct s_expr *eval_list (struct s_expr *list, struct l_env *env,
                          struct file_pos *pos) {
  struct s_expr *expr = NULL, *rest = list;

  for (; rest && rest->type == DOTTED_PAIR; rest = rest->u.pair.rest)
    expr = eval_s_expr(rest->u.pair.first, env, pos);

  if (rest)
    error(NON_LIST_MSG, pos, s_expr_string(list, env));

  return expr;
}

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;
}

struct s_expr *eval_file (char *filename, struct l_env *env) {
  struct file_pos pos, prev_pos;
  struct s_expr *expr;
  FILE *file;
  int chr;

  file = fopen(filename, "r");
  if (!file) {
    printf("Failed to open file '%s'n", filename);
    exit(1);
  }

  pos.filename = filename, pos.line = pos.chr = 1;
  expr = NULL;

  while (1) {
    chr = get_significant_char(file, &pos);
    if (chr == EOF)
      break;
    ungetc(chr, file);
    pos.chr--, prev_pos = pos;
    expr = eval_s_expr(parse_s_expr(file, &pos), env, &prev_pos);
  }

  fclose(file);
  return expr;
}

int main (int argc, char *argv[]) {
  struct l_env *env;

  if (argc != 2) {
    puts("Usage: int source");
    exit(1);
  }

  env = create_env();
  puts(s_expr_string(eval_file(argv[1], env), env));

  return 0;
}

Я решил ввести более лаконичные названия для базовых и произвольных функций и макросов. В классическом 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 ассемблера. Буду пробовать…

Автор: ababo

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js