在比较a的值是否和某个特定的 identifier #'b相同时,一般会直接地(或通过syntax-rules等间接地)使用(free-identifier=? a #'b)。但也有一些情况不能如此处理。
在讨论 phase level 与 identifier 的匹配之前,首先要明确几个概念(在The Racket Reference中有更加详细的解释)。
require的时候,是在相对于 base phase 的环境里引入 binding :直接的require就是在 base phase ,for-template就是 base phase – 1,以此类推……free-identifier=?比较的是两个 identifier 在给定的 phase level 是否有相同的 binding (或者都没有 binding ),而默认的 phase level 是(syntax-local-phase-level)。由上可知,(free-identifier=? a #'b)是否正确,一个关键是(syntax-local-phase-level)是否就是所期望的 phase level 。
根据The Racket Reference的解释:
During the dynamic extent of a syntax transformer application by the expander, the result is the phase level of the form being expanded. Otherwise, the result is 0.
所以这里就有两种特殊情况了:
注:syntax-local-phase-level是有可能得到负数的:
>(modulearacket(begin-for-syntax(displayln(syntax-local-phase-level))))000>(require(for-template'a))-1
#lang racket(modulearacket(define-for-syntaxf1)(define-syntaxdo(syntax-rules(f)[(_f)(begin-for-syntax(displayln1))][(_form)(begin-for-syntax(displayln0))]))(providedo(for-syntaxf)))(require'a)(defineg0)(dof)
这个程序中,a的do宏需要将输入与其上面定义在 phase level 1的f比较。但是,被展开的代码在0(即(syntax-local-phase-level)为0)。
(do f)中的f在 phase level 0没有 binding ,(syntax-rules (f) ...)的f在 phase level 0也没有 binding 。因此,打印的是1。
然而,如果将(define g 0)中的g换成f,那么(do f)中的f在 phase level 0解析到这个 binding 。而(syntax-rules (f) ...)的f仍然没有 binding ,结果就是0了。
所以,这里的实际问题是syntax-rules没有在 phase level 1比较 identifier 。修改方法有:
(define-syntax(dostx)(syntax-case*stx(f)free-transformer-identifier=?[(_f)#'(begin-for-syntax(displayln1))][(_form)#'(begin-for-syntax(displayln0))]))
或
(define-syntax(dostx)(syntax-casestx()[(_f-id)(free-transformer-identifier=?#'f#'f-id)#'(begin-for-syntax(displayln1))][(_form)#'(begin-for-syntax(displayln0))]))
free-transformer-identifier=?在(add 1 (syntax-local-phase-level))进行比较。两边的f在 phase level 1有相同的 binding ,不论外面是不是有 phase level 0的f干扰,都能不受影响输出1。
看如下程序:
#lang racket(modulefooracket/base(define-syntaxfoo(syntax-rules()))(providefoo))(modulearacket/base(require(for-template(submod".."foo)))(definefoo"bad")(define(is-foo?stx)(syntax-casestx(foo)[foo#t][_#f]))(provideis-foo?))(require'foo(for-syntax'a))(define-syntax(fstx)(syntax-casestx()[(_id)(is-foo?#'id)#'#t][_#'#f]))(ffoo)
在module a中,定义了一个辅助函数is-foo?,用来判断一个 identifier 是不是解析到module foo的foo。
is-foo?运行在 phase level 1,因为有for-template,a的syntax-case的foo在 phase level 1 – 1 = 0 解析到module foo的foo。而a定义的foo在 phase level 1,因此不会产生干扰。
(f foo)的foo因为在 phase level 0 require了module foo,所以解析到module foo的foo。
因此二者 binding 相同,结果是#t。
然而,如果要在非宏展开的时候使用这两个module,情况就不一样了:
#lang racket(modulefooracket/base(define-syntaxfoo(syntax-rules()))(providefoo))(modulearacket/base(require(for-template(submod".."foo)))(definefoo"bad")(define(is-foo?stx)(syntax-casestx(foo)[foo#t][_#f]))(provideis-foo?))(require'foo'a)(is-foo?#'foo)
此时(syntax-local-phase-level)仍为0,而module a的foo定义在0,module foo的foo在”–1“。这样一来syntax-case把stx(仍然解析到module foo的foo)与module a定义的foo相比较,得到#f。
就结果而言:
在第一个例子中,module a的 base phase 为1,is-foo?比较了stx和module foo的foo。
在第二个例子中,module a的 base phase 为0,is-foo?比较了stx和module a自己的foo。
也就是说,在不改动module a和module foo的情况下,仅仅是使用方式的不同,就使is-foo?产生了完全不同的行为。
解决这个问题的关键是计算module a的 base phase (使用variable-reference->module-base-phase):
(modulearacket/base(require(for-template(submod".."foo)))(definefoo"bad")(definebase-phase(variable-reference->module-base-phase(#%variable-reference)))(define(is-foo?stx)(free-identifier=?stx#'foo(syntax-local-phase-level)(sub1base-phase)))(provideis-foo?))
这样,is-foo?总是比较stx和module foo的foo。
(另见https://rmculpepper.github.io/blog/2011/09/syntax-parse-and-literals/)
Fully Expanded Program (以下简称FPE)中的 identifier 的 phase level 情况也比较微妙。当然,这里的FPE指的是非 expression context 下的FPE——在 expression context 下,FPE并不涉及任何 phase level 的变化,属于平凡的情况。
非 expression context 的FPE通常有两种来源:
对一个 top-level 的module做完全展开。
#%module-begin对其body做完全展开。
值得注意的是,匹配一个完全展开的 top-level module的情况,是完全可能发生在非宏展开的时候的——各种annotator工作的基本流程就是:expand -> annotate(这里需要进行匹配) -> compile/eval。这个annotate过程正是发生在expander结束工作之后。
要匹配FPE中的 identifier ,假定表达式是(free-identifier=? id lit phase lit-phase)。
这里id是输入的 identifier , lit是程序指定的 literal identifier (应当属于(kernel-form-identifier-list)), 这两个参数没有疑问。而lit-phase 需要保证lit带有正确的 binding ,取值参照前一节即可。
所以关键就是phase的取值了。
根据上面的两种情况,匹配的起点可能是module或者#%plain-module-begin。
module的情况,取决于FPE的来源:
expand/expand-syntax展开得到的,那么phase的初值应当是(namespace-base-phase);local-expand(context-v为'top-level)得到。虽然比较罕见,但也是有可能的。phase的初值应为(syntax-local-phase-level)。并且,非宏展开的情况用不了local-expand,lit-phase也不用考虑非宏展开的情况。#%plain-module-begin肯定是local-expand得到的,所以是(syntax-local-phase-level)。但由于module的body总是从 phase level 0开始,所以也可以直接用0。除了下面提到的情况外,phase都保持不变
begin-for-syntax和define-syntaxes:比较简单,这两个 identifier 本身在phase匹配,但对于begin-for-syntax的”body“和define-syntaxes的”expr“,phase需要加1。
module和module*:这两个 identifier 本身在phase匹配,但对于其”body“,phase为0。
(module* _ #f _)的情况则是特例,虽然其”body“仍是在 phase level 0展开,但最终得到的FPE经过了shift,所以phase无需变动。需要注意的是,对于这些submodule的#%plain-module-begin,根据module的嵌套的情况,phase的变动不固定。但由于#%plain-module-begin的位置相对module/module*固定,所以也并不需要去匹配——直接匹配module/module*,然后跳过#%plain-module-begin处理其”body“即可。
对于匹配FPE,kernel-syntax-case/phase是最方便的工具,可以自动处理好 literal identifier 和其 phase level ,用户只需要提供phase参数即可。
如果不需要考虑进入 phase level 大于0的部分,kernel-syntax-case也可以用。
drcomplete-user-defined库里有一段非常简短的代码,可以作为示例,这里就不再另外提供示例代码。
https://github.com/yjqww6/drcomplete/blob/master/drcomplete-user-defined/private/main.rkt