Как написать расширение макроса в пользовательском LISP в JavaScript

У меня есть общий вопрос, как мне поступить и создать правильную функцию макроэкспанда или макрос.

Это определение моего макроса в интерпретаторе LIPS (вы можете проверить его здесь https://jcubic.github.io/lips/)

function macro_expand(single) {
    return async function(code, args) {
        var env = args['env'] = this;
        async function traverse(node) {
            if (node instanceof Pair && node.car instanceof Symbol) {
                try {
                    var value = env.get(node.car);
                    if (value instanceof Macro && value.defmacro) {
                        var result = await value.invoke(node.cdr, args, true);
                        if (result instanceof Pair) {
                            return result;
                        }
                    }
                } catch (e) {
                    // ignore variables
                }
            }
            var car = node.car;
            if (car instanceof Pair) {
                car = await traverse(car);
            }
            var cdr = node.cdr;
            if (cdr instanceof Pair) {
                cdr = await traverse(cdr);
            }
            var pair = new Pair(car, cdr);
            return pair;
        }
        var new_code = code;
        if (single) {
            return quote((await traverse(code)).car);
        } else {
            while (true) {
                new_code = await traverse(code);
                if (code.toString() === new_code.toString()) {
                    break;
                }
                code = new_code;
            }
            return quote(new_code.car);
        }
    };
}

Проблема в том, что это фиктивный макрос, расширяющий и игнорирующий ошибки переменных, поэтому он не может оценить квазицитатуру макроса, потому что он выдает исключение, что он не может найти переменные. Таким образом, я получаю квазицитату в моем расширенном списке (ПРИМЕЧАНИЕ: код в последней версии даже не пытается расширить квазицитату, потому что он помечен как нерасширяемый).

Каков подход написания макроса расширения? Должен ли я расширить функцию оценки, чтобы работать по-разному при использовании макроса функции расширения?

Я тестировал, как biwascheme создает эту функцию, https://www.biwascheme.org/ но она также не работает, так как я ожидаю, что эта функция будет работать:

это расширить:

biwascheme> (define-macro (foo name . body) `(let ((x ,(symbol->string name))) `(print ,x)))
biwascheme> (macroexpand '(foo bar))
=> ((lambda (x) (cons (quote print) (cons x (quote ())))) "bar")
biwascheme> 

Я ожидаю, что это расширится до:

(let ((x "bar")) (quasiquote (print (unquote x))))

Возвращение моего лиспа:

lips> (define-macro (foo name . body)
          `(let ((x ,(symbol->string name))) `(print ,x)))
;; macroexpand is a macro
lips> (macroexpand (foo bar))
(quasiquote (let ((x (unquote (symbol->string name))))
              (quasiquote (print (unquote x)))))

Даже когда я установил quasiquote как расширяемый, он не расширяет квазицитатуру, потому что он не может найти имена, поэтому он генерирует исключение, которое игнорируется macroexpand.

Любой код, даже псевдокод, будет полезен при написании этой функции или макроса в моем LISP.

РЕДАКТИРОВАТЬ:

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

До:

var rest = __doc__ ? macro.cdr.cdr : macro.cdr;
if (macro_expand) {
    return rest.car;
}
var pair = rest.reduce(function(result, node) {
    return evaluate(node, { env, dynamic_scope, error });
});

после:

var rest = __doc__ ? macro.cdr.cdr : macro.cdr;
var pair = rest.reduce(function(result, node) {
    return evaluate(node, eval_args);
});
if (macro_expand) {
    return quote(pair);
}

И теперь он работает без проблем, поэтому мой макрос expand_macro работает правильно, и вот как вы должны написать macro_expand.

РЕДАКТИРОВАТЬ 2: я далее рефакторинг кода, и оказывается, что мне не нужно код macro_exapnd внутри макроса define-macro и только кавычки пары (флаг удаления данных).

1 ответ

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

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

Специальные формы

Прежде всего нам нужно разобраться со специальными формами. Специальные формы - это вещи, которые имеют магическую семантику. У этого экспандера очень простое представление о том, как они работают:

  • специальная форма - это составная форма, первым элементом которой является специальный оператор;
  • каждый элемент остальной части формы - это какая-то особая вещь, которая не раскрывается, или она раскрывается как обычно, что делается следующим образом: expr в определении;
  • способ сделать это - довольно бессмысленное сопоставление с образцом, которое, вероятно, адекватно только потому, что существует небольшое количество специальных форм, о которых знает экспандер.

Итак, вот как определяются специальные формы, и определение трех из них:

(define special-patterns (make-hasheqv))

(define (special-pattern? op)
  (and (symbol? op)
       (hash-has-key? special-patterns op)))

(define (special-pattern op)
  (hash-ref special-patterns op))

(define-syntax-rule (define-special-pattern (op spec ...))
  (hash-set! special-patterns 'op '(op spec ...)))

(define-special-pattern (quote thing))
(define-special-pattern (lambda args expr ...))
(define-special-pattern (define thing expr ...))
(define-special-pattern (set! thing expr))

Теперь мы можем спросить, является ли что-то специальной формой (специальным шаблоном в коде) и получить ее шаблон:

> (special-pattern? 'lambda)
#t
> (special-pattern 'lambda)
'(lambda args expr ...)

Обратите внимание, что такие вещи, как if не являются специальными операторами для макроэкспандера, хотя на самом деле они особенные: в форме (if test then else) все подчиненные формы должны быть расширены, поэтому у макроэкспандера нет причин знать о них. Это только такие вещи, как lambda где некоторые подчиненные формы не должны быть расширены, о которых нужно знать макроэкспандеру.

Макро определения

Макросы - это составные формы, первый элемент которых распознается как именование макроса. Для каждого такого макроса есть функция макроэкспандера, которая будет отвечать за расширение формы: этой функции передается вся форма. Есть немного синтаксиса, который define-macro, который оборачивает эту функцию таким же образом, что defmacro делает в CL (но нет &whole поддержка или поддержка деструктуризации арглистов или чего-либо в этом роде).

(define macros (make-hasheqv))

(define (macro? op)
  (and (symbol? op)
       (hash-has-key? macros op)))

(define (macro op)
  (hash-ref macros op))

(define-syntax-rule (define-macro (m arg ... . tail) form ...)
  (hash-set! macros 'm (lambda (whole)
                         (apply (lambda (arg ... . tail) form ...)
                                (rest whole)))))

С этим мы можем определить простой макрос: вот четыре определения для let,

Прежде всего, здесь самый элементарный: он даже не использует define-macro но это то, во что она превращается: внешняя функция получает всю форму, а затем вызывает внутреннюю часть, которая не является именем макроса. Внутренняя функция затем кропотливо превращается (let ((x y) ...) ...) в ((lambda (x ...) ...) y ...), который является правильным расширением для let, (Обратите внимание, что ничего из этого не касается CL (let (x) ...)).

(hash-set! macros 'let
           ;; this is what define-macro turns into
           (lambda (whole)
             (apply (lambda (bindings . body)
                      (cons (cons 'lambda
                                  (cons (map first bindings) body))
                            (map second bindings)))
                    (rest whole))))

Теперь вот что, но с помощью define-macro уменьшить боль:

(define-macro (let bindings . body)
  ;; Really primitive version
  (cons (cons 'lambda (cons (map first bindings) body))
        (map second bindings)))

И другая версия с использованием list* сделать вещи немного менее ужасными:

(define-macro (let bindings . body)
  ;; without backquote, but usung list* to make it a bit
  ;; less painful
  (list* (list* 'lambda (map first bindings) body)
         (map second bindings)))

И, наконец, версия с использованием обратной цитаты (или квазицитаты).

(define-macro (let bindings . body)
  ;; with backquote
  `((lambda ,(map first bindings) ,@body)
    ,@(map second bindings)))

Вот версия определения макроса для prog1 который сломан из-за нарушения гигиены:

(define-macro (prog1 form . forms)
  ;; Broken
  `(let ([r ,form])
     ,@forms
     r))

И вот как вы должны написать это, чтобы быть более гигиеничным (хотя он все еще негигиеничен по несколько экстремальным стандартам Схемы):

(define-macro (prog1 form . forms)
  ;; Working
  (let ([rn (string->uninterned-symbol "r")])
    `(let ([,rn ,form])
       ,@forms
       ,rn)))

Обратите внимание, что этот макрос превращается в другой макрос: он расширяется до let: экспандер должен иметь дело с этим (и это делает).

Макроэкспандер

Макроэкспандер состоит из двух функций: expand-macros это то, что на самом деле делает расширение, и оно отправляет expand-special для специальных форм.

Вот expand-macros:

(define (expand-macros form)
  ;; expanding a form
  (if (cons? form)
      ;; only compound forms are even considered
      (let ([op (first form)])
        (cond [(macro? op)
               ;; it's a macro: call the macro function & recurse on the result
               (expand-macros ((macro op) form))]
              [(special-pattern? op)
               ;; it's special: use the special expander
               (expand-special form)]
              [else
               ;; just expand every element.
               (map expand-macros form)]))
      form))

Примечания об этом:

  • только сложные формы могут быть макроформами;
  • это lisp-1, поэтому машины составных форм оцениваются полностью нормально и могут быть макро-формами: ((let (...) ...) ...) Это хорошо;
  • макросы расширяются рекурсивно, пока не останется ничего сделать.

Вот expand-special: это гораздо сложнее, чем expand-macro и, вероятно, глючит: он пытается сопоставить определение специальной формы с формой, которую он получил.

(define (expand-special form)
  ;; expand a special thing based on a pattern.
  (match-let* ([(cons op body) form]
               [(cons pop pbody) (special-pattern op)])
    (unless (eqv? op pop)
      (error 'expand-special "~s is not ~s" pop op))
    (let pattern-loop ([accum (list op)]
                       [tail body]
                       [ptail pbody]
                       [context 'expr])
      (cond [(null? tail)
             (unless (or (null? ptail)
                         (eqv? (first ptail) '...))
               (error 'expand-special "~s is not enough forms for ~s"
                      body op))
             (reverse accum)]
            [(null? ptail)
             (error 'expand-special "~s is too many forms for ~s"
                    body op)]
            [else
             (match-let* ([(cons btf btr) tail]
                          [(cons ptf ptr) ptail]
                          [ellipsis? (eqv? ptf '...)]
                          [ctx (if ellipsis? context ptf)]
                          [ptt (if ellipsis? ptail ptr)])
               (pattern-loop (cons (if (eqv? ctx 'expr)
                                       (expand-macros btf)
                                       btf)
                                   accum)
                             btr ptt ctx))]))))

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

Обратите внимание, что это возвращается в expand-macros где это необходимо, конечно.

Учитывая эти определения, мы можем теперь расширить некоторые макросы:

> (expand-macros '(let ((x y)) x))
'((lambda (x) x) y)
> (expand-macros '(prog1 a b))
'((lambda (r) b r) a)

Обратите внимание, что принтер Racket не печатает специально, а r выше не имеет отношения.

С помощью простой утилиты трассировки вы можете определить отслеживаемую версию макроэкспандера:

> (expand-macros '(let ([x 1]) (prog1 x (display "1"))))
[expand-macros (let ((x 1)) (prog1 x (display "1")))
 [expand-macros ((lambda (x) (prog1 x (display "1"))) 1)
  [expand-macros (lambda (x) (prog1 x (display "1")))
   [expand-special (lambda (x) (prog1 x (display "1")))
    [expand-macros (prog1 x (display "1"))
     [expand-macros (let ((r x)) (display "1") r)
      [expand-macros ((lambda (r) (display "1") r) x)
       [expand-macros (lambda (r) (display "1") r)
        [expand-special (lambda (r) (display "1") r)
         [expand-macros (display "1")
          [expand-macros display
           -> display]
          [expand-macros "1"
           -> "1"]
          -> (display "1")]
         [expand-macros r
          -> r]
         -> (lambda (r) (display "1") r)]
        -> (lambda (r) (display "1") r)]
       [expand-macros x
        -> x]
       -> ((lambda (r) (display "1") r) x)]
      -> ((lambda (r) (display "1") r) x)]
     -> ((lambda (r) (display "1") r) x)]
    -> (lambda (x) ((lambda (r) (display "1") r) x))]
   -> (lambda (x) ((lambda (r) (display "1") r) x))]
  [expand-macros 1
   -> 1]
  -> ((lambda (x) ((lambda (r) (display "1") r) x)) 1)]
 -> ((lambda (x) ((lambda (r) (display "1") r) x)) 1)]
'((lambda (x) ((lambda (r) (display "1") r) x)) 1)

Версия этого кода доступна здесь.

Другие вопросы по тегам