【问题标题】:How to write macro expand in custom defined LISP in JavaScript如何在 JavaScript 中自定义 LISP 中编写宏扩展
【发布时间】:2023-03-12 14:02:03
【问题描述】:

我有一个普遍的问题,我应该如何着手并创建适当的宏扩展函数或宏。

这是我在 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);
        }
    };
}

问题在于这是虚拟宏扩展并忽略有关变量的错误,因此它无法评估宏准引用,因为它会抛出找不到变量的异常。所以我最终在我的扩展列表中添加了 quasiquote(注意:最新版本的代码甚至不尝试扩展 quasiquote,因为它被标记为不可扩展)。

宏展开的写法是什么?使用宏扩展功能时,我应该扩展评估功能以使其工作不同吗?

我正在测试 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))))

我的 lisp 返回:

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 设置为可扩展,它也不会扩展 quasiquote,因为它找不到名称,因此会引发宏扩展忽略的异常。

任何代码甚至伪代码都将有助于在我的 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。

EDIT2:我进一步重构了代码,结果发现我不需要在define-macro 宏中执行macro_exapnd 代码,只需取消引用该对(删除数据标志)。

【问题讨论】:

  • 至少在 Common Lisp 中,MACROEXPAND 多次调用自身,直到表单不再是宏表单。例如,在某些 Lisp 中,LET 可能是宏,然后扩展为 ((lambda (vars...) ...) values...) 形式。 Common Lisp 也有 MACROEXPAND-1,它只做一次宏扩展。
  • 如果准引用在读取时被扩展(!),那么它可能会扩展为带有 CONS、LIST、QUOTE 等的形式。它可能没有明确的 QUASIQUOTE 形式扩展到。许多 Common Lisp 实现将使用一些特定于实现的运算符,以便稍后知道这是一个 quasiquote 形式,例如然后相应地打印。
  • 您将问题与问题不必要的异步/等待逻辑混淆了。首先编写一个工作宏扩展器;然后,如果它通过了所有测试,请考虑在保持测试运行的小型重构测试中“异步”它。
  • 宏扩展器不能只是盲目地遍历树结构并在任何地方识别宏调用。它必须了解 Lisp 方言的所有特殊形式。例如,如果m 是一个宏,那么它不应该在(quote (m)) 中被识别。宏扩展器必须知道 quote 运算符不评估其参数,因此也不扩展该参数。同样,如果你有(let ((m 42)) ..)m 也不能被识别为那里的宏调用。
  • 再次检查宏的展开结果,看是否为宏。它必须反复扩展,直到它被缩减为不再是宏调用的形式。然后必须遍历该表单以在其中进行更多宏扩展。

标签: javascript macros lisp lisp-macros


【解决方案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,也不支持arglist 解构或任何其他)。

(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)))

最后是使用反引号(aka quasiquote)的版本。

(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))

为了更卫生,你需要如何编写它(尽管按照 Scheme 的某些极端标准,它仍然是不卫生的):

(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 的打印机不会专门打印 uninterned,但上面的 r 是 uninterned。

使用简单的跟踪实用程序,您可以定义宏扩展器的跟踪版本:

> (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)

此代码的一个版本可用here

【讨论】:

  • 如果您的定义宏只是将数据结构添加到哈希映射,它们不是真正的宏,不是吗?您不能正常调用它们。对于具有此功能的普通宏,您将需要编写创建宏的宏,然后将内容添加到哈希映射中以备后用。此外,我的 LIPS 没有卫生宏,只有真正的 lisp 宏。我不知道这是否会有用,可能是模式匹配,而 list walker 是要走的路。
  • @jcubic:它们不是底层 Lisp 中的宏,而是在其之上的玩具 Lisp 实现的一部分:在那个玩具 Lisp 中,是的,它们将是宏。 (那个玩具 Lisp 不存在,而且可能永远不会存在。)
猜你喜欢
  • 2011-03-08
  • 1970-01-01
  • 2022-06-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-10
  • 2017-12-04
相关资源
最近更新 更多