You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修改Lisp中的#+和#-读取器宏以适配自定义#@注解?

自定义#+/#-读取器宏以支持#@注解链

简短说明

希望修改#+和#-读取器宏,使其除作用于后续单个符号外,还能处理所有紧跟的以#@开头的注解符号。例如:

#+somefeature
#@someattribute1
#@someattribute2
(defun test-fn () 42)

当somefeature特性不存在时,这段代码不会生成任何函数定义或注解相关的代码。

详细说明

我已实现自定义#@读取器宏,用于转换后续代码。比如:

#@traced
(defun test-fn () 42)

这段代码会生成一个带调试追踪的函数,能把调用参数和返回值写入文件。

但和#+结合使用时出现问题:

#+somefeature
#@traced
(defun test-fn () 42)

当somefeature不存在时,#@traced的转换效果会被抑制,但(defun ...)依然会被正常定义,这不符合预期。

用progn包裹可以解决,但写法繁琐:

#+somefeature
(progn
  #@traced
  (defun test-fn () 42))

我想修改#+和#-的读取器宏,大致逻辑是:

(defun conditional-syntax-reader (stream subchar arg)
  ; 如果条件不满足,消耗所有紧跟的#@注解,再消耗后续的目标符号
)
(setf *readtable* (copy-readtable))
(set-dispatch-macro-character #\# #\+ #'conditional-syntax-reader)
(set-dispatch-macro-character #\# #\- #'conditional-syntax-reader)

但问题在于我不知道如何委托给原始读取器宏,也无法完全重实现它们的复杂逻辑。

我尝试过一种朴素实现:

(defun consume-tokens-recursively (stream)
  (let ((token (read stream t nil t)))
    (when (string= "#@" (subseq (symbol-string token) 0 2))
      (consume-tokens-recursively stream))))
(defun conditional-syntax-reader (stream subchar arg)
  (unless (member (read stream t nil t) *features*)
    (consume-tokens-recursively stream)))

但查到资料指出这种方式不严谨:

#+语法先读取特性规格,若特性为假则跳过后续形式。由于存在用户定义宏字符和#.、#,构造的副作用,跳过形式较为复杂,通过绑定read-suppress为非nil值并调用read函数实现。

那是不是只要在read时绑定(let ((*read-suppress* t)))就能解决问题?

编辑补充

进一步分析发现,核心问题是不同#@注解需要的参数数量不同,无法提前确定要消耗的符号数:

  • #@export需要1个参数:要导出的(defun ...)
  • #@traced需要2个参数:调试级别和要追踪的(defun ...)

示例代码:

#+somefeature
#@export
#@traced 3
(defun test-fn () 42)

实际测试发现,#+/#-在抑制状态下,(read)会返回NIL,导致自定义#@读取器宏逻辑中断:

(defun annotation-syntax-reader (stream subchar arg)
 (case (read stream t nil t)
  ('export
   (let ((defun-form (read stream t nil t)))
    ; 执行导出逻辑
    ))
  ('traced
   (let* ((debug-level (read stream t nil t))
          (defun-form (read stream t nil t)))
    ; 执行追踪逻辑
    ))))

(setf *readtable* (copy-readtable))
(set-dispatch-macro-character #\# #\@ #'annotation-syntax-reader)

#+(or) #@traced 3 (defun test-fn () 42)

当#@traced被#+抑制时,annotation-syntax-reader中的(read)调用会消耗真实符号但返回NIL。case判断因匹配失败不会继续消耗3和(defun ...),导致控制权离开#+作用域后,函数定义被正常执行,这显然不符合预期。

解决方案

要解决这个问题,需要从两方面调整:

1. 修改#@读取器宏,支持抑制状态下的完整参数消耗

让#@读取器宏在*read-suppress*为真时,直接跳过该注解所需的所有参数,确保整个注解块被正确消耗,不会留下未处理的符号:

(defun annotation-syntax-reader (stream subchar arg)
  (if *read-suppress*
      ; 抑制状态:跳过注解所需的所有输入
      (case (read stream t nil t)
        ('export (read stream t nil t))  ; 跳过目标函数定义
        ('traced (read stream t nil t)   ; 跳过调试级别
                 (read stream t nil t))) ; 跳过目标函数定义
      ; 正常状态:执行注解转换逻辑
      (case (read stream t nil t)
        ('export
         (let ((defun-form (read stream t nil t)))
           `(progn ,defun-form (export ',(second defun-form)))))
        ('traced
         (let* ((debug-level (read stream t nil t))
                (defun-form (read stream t nil t)))
           `(defun ,(second defun-form) ,(third defun-form)
              (let ((args (list ,@(third defun-form))))
                (format t "[TRACED] ~a called with args: ~a~%" ',(second defun-form) args)
                (let ((result (progn ,@(cdddr defun-form))))
                  (format t "[TRACED] ~a returned: ~a~%" ',(second defun-form) result)
                  result))))))))

2. 包装#+/#-读取器宏,支持连续#@注解的跳过

自定义conditional-syntax-reader,先判断特性是否存在:

  • 特性存在时,读取所有连续的#@注解和目标形式并包装为progn执行
  • 特性不存在时,绑定*read-suppress*为t,循环读取所有连续的#@注解(此时annotation-syntax-reader会自动跳过参数),最后读取并跳过目标形式

实现代码:

(defun conditional-syntax-reader (stream subchar arg)
  (let* ((feature-spec (read stream t nil t))
         (active (ecase subchar
                   (#\+ (member feature-spec *features* :test #'equal))
                   (#\- (not (member feature-spec *features* :test #'equal))))))
    (if active
        ; 激活状态:读取所有连续的#@注解和目标形式,包装为progn执行
        (let ((forms '()))
          (loop
            (let ((form (read stream t nil t)))
              (push form forms)
              ; 检查下一个字符是否是#@开头的注解
              (peek-char nil stream t nil t)
              (unless (and (char= (peek-char nil stream t nil t) #\#)
                           (progn (read-char stream t nil t)
                                  (char= (peek-char nil stream t nil t) #\@)))
                (unread-char #\# stream)
                (return))))
          `(progn ,@(nreverse forms)))
        ; 非激活状态:绑定*read-suppress*跳过所有连续#@注解和目标形式
        (let ((*read-suppress* t))
          (loop
            (peek-char nil stream t nil t)
            ; 检查是否是#@注解
            (unless (and (char= (peek-char nil stream t nil t) #\#)
                         (progn (read-char stream t nil t)
                                (char= (peek-char nil stream t nil t) #\@)))
              (unread-char #\# stream)
              (return))
            ; 跳过#@注解及其参数
            (read stream t nil t))
          ; 跳过目标形式
          (read stream t nil t)
          ; 返回nil,使读取结果被忽略
          nil))))

; 配置读取表
(setf *readtable* (copy-readtable))
(set-dispatch-macro-character #\# #\+ #'conditional-syntax-reader)
(set-dispatch-macro-character #\# #\- #'conditional-syntax-reader)
(set-dispatch-macro-character #\# #\@ #'annotation-syntax-reader)

效果验证

当somefeature不存在时,执行以下代码不会生成任何函数定义:

#+somefeature
#@export
#@traced 3
(defun test-fn () 42)

当somefeature存在时,会生成带追踪和导出的函数,符合预期。

内容的提问来源于stack exchange,提问作者Sod Almighty

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 10:27:46