如何修改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

