scheme-langserver

目标

本次工作需要调试scheme-langserver的宏解析功能,以捕获对identifier的声明过程,便于通过LSP(Language Server Protocol)提供goto definition、find references、auto complete功能。你需要完整阅读本文件,然后使用后文所提到的脚本和调试方法,去完成调试。

待解析宏的案例

akku包管理器下的(ufo-try)库,try宏的调用可以采用下列方式:

(try
    do something here
    (except condition
        [branch to process condition here]
        other branches ...
    )
)

其中的c就是try宏声明的一个identifier,它将捕获程序抛出的异常。

原理

用户自定义宏经过scheme REPL展开、转换后,将形成完全以scheme primitive宏和 primitive过程(procedure)组织的代码。在这些primitive宏中,let\define等将能够声明identifier。

scheme-langserver的思路

scheme-langserver希望在branch中提供对c的goto definition、find references、auto complete等服务。为了实现这一点,需要理清用户自定义宏的调用,和宏调用的展开之间的对应关系。 仍然以(ufo-try),其定义是

(define-syntax try
  (lambda (x)
    (syntax-case x (except)
      [(try body0 body1 ... (except condition clause0 clause1 ...))
        #`((call/1cc
        (lambda (escape)
          (with-exception-handler
            (lambda (c)
          (let ([condition c])     ;; clauses may set! this
            #,(let loop ([first #'clause0] [rest #'(clause1 ...)])
                (if (null? rest)
                (syntax-case first (else =>)
                  [(else h0 h1 ...) #'(escape (lambda () h0 h1 ...))]
                  [(tst) #'(let ([t tst]) (if t (escape (lambda () t)) (raise c)))]
                  [(tst => l) #'(let ([t tst]) (if t (escape (lambda () (l t))) (raise c)))]
                  [(tst h0 h1 ...) #'(if tst (escape (lambda () h0 h1 ...)) (raise c))])
                (syntax-case first (=>)
                  [(tst) #`(let ([t tst]) (if t (escape (lambda () t)) #,(loop (car rest) (cdr rest))))]
                  [(tst => l) #`(let ([t tst]) (if t (escape (lambda () (l t))) #,(loop (car rest) (cdr rest))))]
                  [(tst h0 h1 ...) #`(if tst (escape (lambda () h0 h1 ...)) #,(loop (car rest) (cdr rest)))])))))
            (lambda ()
          ;; cater for multiple return values
          (call-with-values
              (lambda () body0 body1 ...)
            (lambda args
              (escape (lambda ()
                    (apply values args))))))))))])))

显然,其中的(let ([condition c]) …)代码指出,对try的宏调用,展开后将通过let宏声明一个identifier名为condition,并且可以被调用。这就形成了展开后的condition和调用中的condition的对应关系,识别出来即可。

现状

已有功能

通过阅读README.md和doc/目录下的各个文件夹可知目前scheme-langserver已经实现了对primitive宏声明identifier的支持,

  1. 核心机制是/analysis/abstract-interpreter.sls
  2. 对于primitive宏的处理规则主要在/analysis/identifier/rules下面
  3. 处理结果形成了索引,通过/virtual-file-system数据结构保存
  4. LSP相关服务支持,在/protocol文件夹下面
  5. 主文件是scheme-langserver.sls

开发状态

正在开发scheme-langserver的宏解析功能,包括

  1. /analysis/identifier/self-defined-rules/router.sls,负责由abstract-interpreter岔开程序执行通路,将用户自定义宏另例处理;
  2. router.sls将转发akku (ufo-match)中对match宏的调用供调试,也就是说我们调试的时候,目前只看match宏是否被正常处理;
  3. match宏的定义全部由syntax-rules实现,这个和try宏不一样,但是scheme-langserver处理的原理不会改变;
  4. /analysis/identifier/expanders/expansion-wrap.sls的expansion-generator->rule将处理match宏的调用,这边为什么是由这个过程处理,你可以看一下/analysis/identifier/rules/syntax-rules.sls和define-syntax.sls;
  5. /analysis/identifier/expanders包含了宏解析的主要功能。

难点

宏定义往往是嵌套的。仍然以ufo-match宏为例,它的定义是大量的syntax-rules宏互相调用和递归调用。scheme-langserver处理的方法是,对每一层嵌套:

  1. 识别这层嵌套展开前后的代码对应关系;
  2. 调用abstract-interpreter.sls识别展开后代码中的identifier声明;
  3. expansion-wrap.sls中有个shallow-copy,基于对应关系将展开后的声明代入到展开前的宏调用代码。

这样,就形成了递归。

调试脚本

调试环境的准备

bash .akku/env

执行脚本

scheme –script ./tests/analysis/identifier/self-defined-rules/test-router.sps

脚本输出和问题

当前脚本输出包含下面这段

enter-wrap0
(match expression
  [(_ (fuzzy0 **1) fuzzy1 ...)
   (fold-left
     (lambda (exclude-list identifier-parent-index-node)
       (let* ([identifier-index-node (car (index-node-children
                                            identifier-parent-index-node))]
              [target-identifier-reference (let-parameter-process index-node
                                             identifier-index-node
                                             index-node document type)]
              [extended-exclude-list (append
                                       exclude-list
                                       target-identifier-reference)])
         (index-node-excluded-references-set!
           (cadr (index-node-children index-node))
           extended-exclude-list)
         extended-exclude-list))
     '()
     (filter
       (lambda (i) (not (null? (index-node-children i))))
       (index-node-children
         (cadr (index-node-children index-node)))))]
  [else '()])
enter-wrap1
trigger

后面按道理应当出现step-into-next-level-step?,但是没有出现。问题是什么?

调试方法

在代码中适当通过pretty-print、display等方法打印信息,通过观察程序输出,确定代码分支是否正常执行。


自动宏解析的验证与当前决策(2026-05-04)

新增测试文件

测试文件 目的
tests/analysis/identifier/test-match-expansion-compare.sps 比较 scheme-langserver 与 Chez Scheme 对 match 宏第一层展开的结果
tests/analysis/identifier/test-ufo-match-auxiliary-expansion.sps 验证 match-nextmatch-onematch-twomatch-drop-idsmatch-gen-or-step 等辅助宏的一层展开正确性

验证结论

1. 第一层展开正确

scheme-langserversyntax-rules->generator:map+expansion(通过 confirm-clause 验证)对 ufo-match第一层展开结果与 Chez Scheme syntax-case 的匹配结果完全一致。例如 match 宏对 (match atom (pat . body) ...) 展开为 (let ((v atom)) (match-next v ...))

2. 辅助宏展开同样正确

ufo-match 的 17 个代表性辅助宏调用场景进行测试,全部通过。测试覆盖:

这说明 syntax-rules->generator:map+expansion 本身对各层辅助宏的展开逻辑是正确的

3. 超时根因:级联展开无深度限制

test-auto-macro-resolve.sps 的超时问题不是展开逻辑错误,而是 expansion-generator->rule(位于 analysis/identifier/expanders/expansion-wrap.sls)在展开后无限制地调用 step 继续处理下一层宏调用:

match → match-next → match-one → match-two → match-check-ellipsis → match-extract-vars → ...

ufo-match 是一个“宏家族”match 展开后产生 match-next 调用,match-next 展开后产生 match-one 调用,match-one 展开后产生 match-two 调用……每一层展开都会引入新的宏调用,形成深度数十层的级联。expansion-generator->rule 中的 memory 只防止同一表达式的重复展开,但无法阻止不同表达式的链式展开。

作为对比,simple-lettest-auto-resolve-basic.sps 中保留了原 test-simple-macro-auto-resolve.sps 的测试用例)展开后直接变成 primitive let,不再引入新宏调用,因此不会级联。

4. 已知限制:ellipse-pair-form 处理

syntax-rules->generator:map+expansion 中的 private:expansion+index-node->pairsellipse-pair-form(如 (p ...) 展开为多个元素)存在长度不匹配问题。这会导致某些含 ellipsis 的宏调用无法正确建立展开前后的节点对应关系,进而影响 shallow-copy 的引用回传。

已修复private:expansion+index-node->pairs 现已添加 private:take 截断逻辑,当 compound-listchildren 长度不匹配时,截断较短的列表继续配对,避免 map 直接 crash。

当前决策

暂时不启用 router.sls 中的自动宏解析路径(即保持 match-process 等手写规则,不启用 expansion-generator->rule 的通用自动展开)。

原因:

  1. ufo-match 这种宏家族,自动展开会导致级联超时。
  2. ellipse-pair-form 的节点对应关系尚未完善。

router.sls 中相关代码已注释保留(见 analysis/identifier/self-defined-rules/router.sls:51-65),以便未来有新办法时重新开发。

未来可能的开发方向

  1. 展开深度限制:在 expansion-generator->rule 中增加 depth 参数(如最大 2-3 层),超过后停止自动展开,回退到手写规则或保守处理。
  2. 宏家族白名单/黑名单:识别 ufo-match 这类宏家族,对它们直接跳过自动展开,始终使用手写规则。
  3. 完善 ellipse-pair-form:修复 private:expansion+index-node->pairs 对 ellipsis 展开的长度不匹配问题。
  4. 按需展开:只在需要解析 identifier 声明的特定宏调用处触发展开,而非对整个文件的所有宏调用都展开。