PAYATHON 2026

Racket 的 provide 为何报错,导出的宏为何出现未绑定标识符?

排障赵师傅

provide 报错是因为导出阶段与定义阶段不一致:define-syntax 在当前阶段建立语法绑定,而 (provide (for-syntax _RELEASED)) 要求导出高一个阶段的绑定。

代码中还有一个独立问题:(syntax-e #'_RELEASED) 得到的是符号 _RELEASED,不会读取它绑定的布尔值,所以这个条件始终为真。

第二个例子中的 (provide fn) 写法正确。在完整的 Racket 模块中,用普通 require 就能导入并使用这个宏。仅凭重启 DrRacket 后恢复正常,无法确定之前出现未绑定错误的原因。

为什么 define-syntax 与 for-syntax 对不上

这段代码涉及两个阶段:

  • phase 0:模块的普通运行阶段。
  • phase 1:展开该模块中的宏时,执行转换器代码的阶段。

这条定义:

(define-syntax _RELEASED #t)

在 phase 0 建立名为 _RELEASED 的语法绑定,右侧表达式则在 phase 1 求值。右侧在编译期求值,并不意味着 _RELEASED 是 phase 1 的普通变量。

这条导出声明:

(provide (for-syntax _RELEASED))

要求模块中存在 phase 1 的 _RELEASED 绑定。原代码没有定义这个绑定,因此报错:

provide: provided identifier is not defined or required

for-syntax 用于指定阶段。宏相关的名字并不都需要加上它,普通宏通常直接用 (provide 宏名) 导出。

将发布标志定义为编译期变量

如果 _RELEASED 只供 tc 的转换器读取,可以把它放进 begin-for-syntax,只导出 tc:

;; config.rkt
#lang racket/base

(require (for-syntax racket/base))

(provide tc)

(begin-for-syntax
  (define _RELEASED #t))

(define-syntax (tc stx)
  (syntax-case stx ()
    [(_)
     #'(void)]
    [(_ body ...)
     (if _RELEASED
         #'(void)
         #'(begin body ...))]))

begin-for-syntax 中的 define 建立了 phase 1 的普通变量。tc 的转换器也在这个阶段执行,因此可以直接读取 _RELEASED。

(require (for-syntax racket/base)) 引入转换器代码所需的编译期绑定。在 #lang racket/base 下,应显式写出这个依赖。

调用模块的代码如下:

;; main.rkt
#lang racket/base

(require "config.rkt")

(tc
  (displayln "running tests")
  (error 'test "test failed"))

(displayln "program started")

当 _RELEASED 为 #t 时,整个 tc 调用展开为:

(void)

测试代码不会进入后续的展开和执行过程。将 _RELEASED 改为 #f 后,带有测试内容的调用会展开为:

(begin
  (displayln "running tests")
  (error 'test "test failed"))

这时,测试代码会在程序运行时执行;编译期只决定保留还是丢弃这些代码。

如果其他模块也需要读取这个编译期配置,可以将前面的导出声明改为:

(provide tc
         (for-syntax _RELEASED))

此时,begin-for-syntax 已将 _RELEASED 定义在 phase 1,导出阶段与定义阶段一致。如果它只供 tc 内部使用,就无须导出。

为什么 syntax-e 不能读取发布标志

原代码中的条件是:

(if (syntax-e #'_RELEASED)
    #'(void)
    #'(begin body ...))

#'_RELEASED 是包含标识符的语法对象。对它调用 syntax-e,得到的是符号:

'_RELEASED

syntax-e 不会查询标识符的绑定值,也不会对它求值。Racket 中只有 #f 被视为假,符号 _RELEASED 为真,所以这个分支始终选择 #'(void)。

因此,修复 provide 后,还需要修改这个条件。采用前面的编译期变量方案,直接读取 _RELEASED 即可:

(if _RELEASED
    #'(void)
    #'(begin body ...))

fn 宏可以直接通过 provide 导出

补上模块语言声明后,这两个文件就是一个完整示例:

;; common.rkt
#lang racket/base

(provide fn)

(define-syntax fn
  (syntax-rules ()
    [(_ . args)
     (lambda . args)]))
;; a.rkt
#lang racket/base

(require "common.rkt")

((fn (x) (+ x 1)) 3)

其中的宏调用:

(fn (x) (+ x 1))

展开为:

(lambda (x) (+ x 1))

最后一个表达式的值为 4。

这里用普通 (provide fn) 和 (require "common.rkt") 即可。如果把导入改成:

(require (for-syntax "common.rkt"))

就会将 fn 的绑定移到高一个阶段,无法在示例中的普通表达式位置使用。

重启 DrRacket 后恢复,应该如何排查

题目没有提供出错时的完整文件、语言设置和操作过程,无法据此认定是缓存问题或 DrRacket 缺陷。重启后成功,也不能证明 provide 曾经失效。

可以按以下顺序排查:

  1. 确认两个文件都以预期的 #lang 声明开头,并已保存到磁盘。

  2. 确认 "common.rkt" 相对于 a.rkt 指向预期文件,避免加载另一个同名文件。

  3. 在 DrRacket 中对 a.rkt 点击 Run,重新运行当前模块,不要只在旧的交互环境中执行最后一条表达式。

  4. 在这两个文件所在目录执行:

    racket a.rkt

如果完整示例能在命令行运行,但某个 DrRacket 会话仍然报错,就需要检查该会话的语言设置、已加载的定义,以及实际运行的文件,才能判断具体原因。

发布标志决定宏展开的结果。修改 _RELEASED 后,使用 tc 的模块需要重新展开或编译,才能生成对应的代码;已经生成的代码不会在运行时自动切换测试模式。

备注:内容仅供参考。