在Common Lisp包中导出指代宏

5

我遇到了导出宏的问题,当它在同一个包中声明时可以正常工作,但是当它被导入时就不能正常工作。我在Windows上使用Emacs、SLIME和Clozure。

包文件

(defpackage :tokenizer
  (:use :common-lisp)
  (:export :tokenize-with-symbols 
       :current-token 
       :advanze-token 
       :peek-token
       :with-token-and-peek
       :with-token))

(defpackage :csharp-parser
  (:use :common-lisp :tokenizer)
  (:import-from :tokenizer :with-token-and-peek :with-token))

分词器文件

(in-package :tokenizer)

(defmacro with-token-and-peek (&body body) 
  `(let ((token (current-token tokenizer))
     (peek (peek-token tokenizer)))
     ,@body))

解析器文件

(in-package :csharp-parser)

(defun expression (tokenizer node-stack)
  (with-token-and-peek
   (cond ((is-number? token) (make-value-node "number" token))
         ((is-bool? token) (make-value-node "bool" token))
         ((is-identifier? token peek) (make-identifier-node tokenizer node-stack))
         (t (make-ast-node :identifier "bla")))))

编译时出现错误:
csharpParser.lisp:265:3:
  warning: Undeclared free variable TOKENIZER::TOKENIZER (2 references)
           style-warning: Unused lexical variable TOKENIZER::PEEK
           style-warning: Unused lexical variable TOKENIZER::TOKEN
csharpParser.lisp:266:14:
  warning: Undeclared free variable TOKEN
etc etc etc

如果我在:csharp-parser包中尝试宏展开。
(macroexpand-1 '(with-token-and-peek tok))

(LET ((TOKENIZER::TOKEN (CURRENT-TOKEN TOKENIZER::TOKENIZER))
      (TOKENIZER::PEEK (PEEK-TOKEN TOKENIZER::TOKENIZER)))
  TOK)
T

现在,就像我说的那样,如果我将宏移动到解析器文件中,它会编译并且运行得非常完美。但是当我尝试将其重构为标记化文件并通过包系统导出时,它会出现这些错误,因为它似乎将符号内部化到调用包中。我已经尝试了多种方法,使用冒号,但无法使其工作。

如果有人能帮助我解决这个问题,我将不胜感激。

2个回答

8
宏中的符号TOKENPEEK是在TOKENIZER包中引入的,而COND内部的代码使用了在CSHARP-PARSER包中引入的符号。有两种解决方法。
  1. Have the expansion use a symbol interned in the package where the code is. This can be done by manually interning a symbol in the current package while expanding the macro. For example:

    (defpackage #:foo
      (:use #:cl)
      (:export #:aif))
    
    (in-package #:foo)
    
    (defmacro aif (test then &optional else)
      (let ((it (intern (symbol-name 'it))))
        `(let ((,it ,test))
           (if ,it ,then ,else))))
    
    (in-package :cl-user)
    (use-package :foo)
    (aif (+ 3 3) it) ;=> 6
    

    Using (intern (symbol-name 'it)) instead of just (intern "IT") is a way of avoiding problems in case the lisp doesn't convert symbols to uppercase.

  2. Have the code use the symbol interned in the tokenizer package. This can be done by exporting the symbol.

    (defpackage #:foo
      (:use #:cl)
      (:export #:aif
               #:it))
    
    (in-package #:foo)
    
    (defmacro aif (test then &optional else)
      `(let ((it ,test))
         (if it ,then ,else)))
    
    (in-package :cl-user)
    (use-package :foo)
    (aif (+ 3 3) it) ;=> 6
    

    The drawback is that the user of the macro must import the symbol, so they can't use the package qualified name for the macro.

    (defpackage #:foo
      (:use #:cl)
      (:export #:aif
               #:it))
    
    (in-package #:foo)
    
    (defmacro aif (test then &optional else)
      `(let ((it ,test))
         (if it ,then ,else)))
    
    (in-package :cl-user)
    (foo:aif (+ 3 3) it) ; Fails
    

谢谢!第一个解决方案很好,已经编译通过了! - HeinzGuderian

2
这是因为带有内部符号的宏扩展出的代码包含在TOKENIZER中内部化的符号,而cond表达式仅包含在CSHARP-PARSER中内部化的符号。任何宏扩展包含的符号(除关键字或gensym之外)都应该被内部化。
下面的宏将一组符号内部化为相同名称的变量:
(defmacro with-interned-symbols (symbol-list &body body)
  "Interns a set of symbols in the current package to variables of the same (symbol-name)."
  (let ((symbol-list (mapcar (lambda (s)
                               (list s `(intern (symbol-name ',s))))
                             symbol-list)))
    `(let ,symbol-list ,@body)))

使用上述方法,可以通过重新定义带有令牌和预读的宏来避免此类不匹配:
(with-interned-symbols (token peek)
  (defmacro with-token-and-peek (&body body) 
    `(let ((,token (current-token tokenizer))
       (,peek (peek-token tokenizer)))
       ,@body)))

请注意,虽然指代宏可能是这里最明显的特殊情况,但任何引入新符号到扩展中的宏都可能发生这种情况,关键字除外,因为它们始终在关键字包中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接