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

如何在Common Lisp中解析未加载依赖包的Lisp文件?

实现无依赖的Common Lisp源码S表达式读取

完全可以做到在不加载依赖包、不验证符号存在性的前提下,把CL源码完整读取为S表达式,而且这是比sed替换靠谱得多的方案——毕竟CL的读取器是处理自身语法的权威工具,能覆盖所有语法细节,不需要你自己啃复杂的语法规则。下面分步骤给你讲具体实现:

1. 解决未定义包的错误

当读取带包前缀的符号(比如woo.ev.tcp:start-listening-socket)时,如果包不存在,CL会抛出package-does-not-exist条件。我们可以通过捕获这个条件,动态创建空包来解决:

(handler-bind ((package-does-not-exist
                 (lambda (condition)
                   ;; 获取不存在的包名,创建一个空包(不继承任何其他包的符号)
                   (let ((pkg-name (package-error-package condition)))
                     (make-package pkg-name :use '())
                     ;; 让读取器继续处理,此时包已经存在
                     (continue condition)))))
  ;; 这里放读取文件的代码
  )

这个逻辑很简单:遇到不存在的包,就自动创建一个空的,读取器就能正常处理带该包前缀的符号了——而且符号会被正确intern到对应的空包中,保留完整的包前缀信息。

2. 保留条件编译标记(#+/#-)

默认情况下,CL读取器会根据当前Lisp的*features*列表,决定是否跳过#+/#-后面的内容。但你需要保留这些标记的原始结构,所以要重新定义对应的读取宏:

;; 先保存原始的#+读取宏,之后要恢复
(let ((original-plus-macro (get-dispatch-macro-character #\# #\+)))
  (set-dispatch-macro-character
   #\# #\+
   (lambda (stream char arg)
     (declare (ignore char arg))
     ;; 读取条件标签和后续的表达式,包装成列表
     (list '#+ (read stream t nil t) (read stream t nil t)))))

;; 同样处理#-
(let ((original-minus-macro (get-dispatch-macro-character #\# #\-)))
  (set-dispatch-macro-character
   #\# #\-
   (lambda (stream char arg)
     (declare (ignore char arg))
     (list '#- (read stream t nil t) (read stream t nil t)))))

这样,读取#+sbcl (print "hello")时,就会得到(#+ sbcl (print "hello")),而不是根据当前环境跳过或直接读取内部表达式。

3. 整合所有逻辑的完整读取函数

把上面的处理整合起来,写一个可以直接调用的函数,同时保证不会破坏当前Lisp会话的默认行为(比如用完后恢复原始读取宏):

(defun read-lisp-source-preserved (file-path)
  ;; 保存原始的读取宏,避免影响后续操作
  (let ((original-plus (get-dispatch-macro-character #\# #\+))
        (original-minus (get-dispatch-macro-character #\# #\-))
        (original-dot (get-dispatch-macro-character #\# #\.)) ; 可选:处理#.求值表达式
        (*read-eval* nil)) ; 禁止自动求值#.表达式,避免安全问题
    ;; 重新定义#+/#-读取宏以保留结构
    (set-dispatch-macro-character
     #\# #\+
     (lambda (stream char arg)
       (declare (ignore char arg))
       (list '#+ (read stream t nil t) (read stream t nil t))))
    (set-dispatch-macro-character
     #\# #\-
     (lambda (stream char arg)
       (declare (ignore char arg))
       (list '#- (read stream t nil t) (read stream t nil t))))
    ;; 可选:重新定义#.读取宏以保留求值表达式结构
    (set-dispatch-macro-character
     #\# #\.
     (lambda (stream char arg)
       (declare (ignore char arg))
       (list '#. (read stream t nil t))))
    
    (unwind-protect
         ;; 处理未定义包 + 读取整个文件的S表达式
         (handler-bind ((package-does-not-exist
                          (lambda (c)
                            (make-package (package-error-package c) :use '())
                            (continue c)))
                        (read-eval-not-allowed
                          (lambda (c)
                            ;; 如果没重新定义#.,捕获这个错误并保留结构
                            (declare (ignore c))
                            (list '#. (read stream t nil t)))))
           (with-open-file (stream file-path :direction :input)
             (loop for form = (read stream nil nil)
                   while form
                   collect form)))
      ;; 恢复所有原始读取宏
      (set-dispatch-macro-character #\# #\+ original-plus)
      (set-dispatch-macro-character #\# #\- original-minus)
      (set-dispatch-macro-character #\# #\. original-dot))))

4. 后续导出到JSON的注意事项

用cl-json导出时,符号默认会被转换成小写字符串,但你可能需要保留包前缀信息。可以自定义符号的JSON序列化规则:

(defmethod cl-json:encode-json ((sym symbol) &optional stream)
  (cl-json:encode-json
   (if (symbol-package sym)
       (format nil "~A:~A" (package-name (symbol-package sym)) (symbol-name sym))
       (symbol-name sym))
   stream))

如果需要保留原始符号的大小写,在读取文件前加上(setf *readtable-case* :preserve)即可,这样CL读取器会保留源码中的大小写格式。

为什么不推荐sed方案?

sed这类文本替换工具完全不理解CL语法:比如字符串里的"woo.ev.tcp:foo"会被错误替换,带特殊字符的符号(比如|woo.ev.tcp:start|)也会处理失败,更别说嵌套的语法结构了。用CL自身的读取器是唯一能正确处理所有语法细节的方案。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 08:02:11