Phase 与 identifier的匹配

在比较a的值是否和某个特定的 identifier #'b相同时,一般会直接地(或通过syntax-rules等间接地)使用(free-identifier=? a #'b)。但也有一些情况不能如此处理。

在讨论 phase levelidentifier 的匹配之前,首先要明确几个概念(在The Racket Reference中有更加详细的解释)。

由上可知,(free-identifier=? a #'b)是否正确,一个关键是(syntax-local-phase-level)是否就是所期望的 phase level

syntax-local-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是有可能得到负数的:

> (module a racket
    (begin-for-syntax
      (displayln (syntax-local-phase-level))))
0
0
0
> (require (for-template 'a))
-1

Phase Level 不一致的情况

#lang racket

(module a racket
  (define-for-syntax f 1)
  
  (define-syntax do
    (syntax-rules (f)
      [(_ f) (begin-for-syntax (displayln 1))]
      [(_ form) (begin-for-syntax (displayln 0))]))

  (provide do (for-syntax f)))

(require 'a)

(define g 0)
(do f)

这个程序中,a的do宏需要将输入与其上面定义在 phase level 1的f比较。但是,被展开的代码在0(即(syntax-local-phase-level)0)。

(do f)中的fphase level 0没有 binding(syntax-rules (f) ...)fphase level 0也没有 binding 。因此,打印的是1

然而,如果将(define g 0)中的g换成f,那么(do f)中的fphase level 0解析到这个 binding 。而(syntax-rules (f) ...)f仍然没有 binding ,结果就是0了。

所以,这里的实际问题是syntax-rules没有在 phase level 1比较 identifier 。修改方法有:

(define-syntax (do stx)
  (syntax-case* stx (f) free-transformer-identifier=?
    [(_ f) #'(begin-for-syntax (displayln 1))]
    [(_ form) #'(begin-for-syntax (displayln 0))]))

(define-syntax (do stx)
  (syntax-case stx ()
    [(_ f-id)
     (free-transformer-identifier=? #'f #'f-id)
     #'(begin-for-syntax (displayln 1))]
    [(_ form)
     #'(begin-for-syntax (displayln 0))]))

free-transformer-identifier=?(add 1 (syntax-local-phase-level))进行比较。两边的fphase level 1有相同的 binding ,不论外面是不是有 phase level 0的f干扰,都能不受影响输出1

非宏展开的情况

看如下程序:

#lang racket
(module foo racket/base
  (define-syntax foo (syntax-rules ()))
  (provide foo))

(module a racket/base
  (require (for-template (submod ".." foo)))
  
  (define foo "bad")
  
  (define (is-foo? stx)
    (syntax-case stx (foo)
      [foo #t]
      [_ #f]))
  (provide is-foo?))

(require 'foo (for-syntax 'a))

(define-syntax (f stx)
  (syntax-case stx ()
    [(_ id)
     (is-foo? #'id)
     #'#t]
    [_ #'#f]))

(f foo)

在module a中,定义了一个辅助函数is-foo?,用来判断一个 identifier 是不是解析到module foo的foo

is-foo?运行在 phase level 1,因为有for-template,a的syntax-casefoophase level 1 – 1 = 0 解析到module foo的foo。而a定义的foophase level 1,因此不会产生干扰。

(f foo)foo因为在 phase level 0 require了module foo,所以解析到module foo的foo

因此二者 binding 相同,结果是#t

然而,如果要在非宏展开的时候使用这两个module,情况就不一样了:

#lang racket
(module foo racket/base
  (define-syntax foo (syntax-rules ()))
  (provide foo))

(module a racket/base
  (require (for-template (submod ".." foo)))
  
  (define foo "bad")
  
  (define (is-foo? stx)
    (syntax-case stx (foo)
      [foo #t]
      [_ #f]))
  (provide is-foo?))

(require 'foo 'a)

(is-foo? #'foo)

此时(syntax-local-phase-level)仍为0,而module a的foo定义在0,module foo的foo在”–1“。这样一来syntax-casestx(仍然解析到module foo的foo)与module a定义的foo相比较,得到#f

就结果而言:

也就是说,在不改动module a和module foo的情况下,仅仅是使用方式的不同,就使is-foo?产生了完全不同的行为。

解决这个问题的关键是计算module a的 base phase (使用variable-reference->module-base-phase):

(module a racket/base
  (require (for-template (submod ".." foo)))
  
  (define foo "bad")

  (define base-phase
    (variable-reference->module-base-phase
     (#%variable-reference)))
  
  (define (is-foo? stx)
    (free-identifier=?
     stx #'foo
     (syntax-local-phase-level)
     (sub1 base-phase)))
  
  (provide is-foo?))

这样,is-foo?总是比较stx和module foo的foo

(另见https://rmculpepper.github.io/blog/2011/09/syntax-parse-and-literals/

匹配Fully Expanded Program

Fully Expanded Program (以下简称FPE)中的 identifierphase level 情况也比较微妙。当然,这里的FPE指的是非 expression context 下的FPE——在 expression context 下,FPE并不涉及任何 phase level 的变化,属于平凡的情况。

expression context 的FPE通常有两种来源:

值得注意的是,匹配一个完全展开的 top-level module的情况,是完全可能发生在非宏展开的时候的——各种annotator工作的基本流程就是:expand -> annotate(这里需要进行匹配) -> compile/eval。这个annotate过程正是发生在expander结束工作之后。

要匹配FPE中的 identifier ,假定表达式是(free-identifier=? id lit phase lit-phase)

这里id是输入的 identifierlit是程序指定的 literal identifier (应当属于(kernel-form-identifier-list)), 这两个参数没有疑问。而lit-phase 需要保证lit带有正确的 binding ,取值参照前一节即可。

所以关键就是phase的取值了。

phase的初值

根据上面的两种情况,匹配的起点可能是module或者#%plain-module-begin

phase的变化

除了下面提到的情况外,phase都保持不变

kernel-syntax-case/phase

对于匹配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