在DrRacket里,当光标移动到一个名字上时,会出现从其使用指向其定义的箭头。这个箭头可以辅助代码阅读,也预示了“变量重命名”功能的作用范围。
由于宏的存在,一部分的 identifier 会在宏返回的syntax对象中丢失,因此需要在宏返回的syntax对象的 syntax property 里追加相应的信息。涉及的 syntax property 有:
disappeared-usedisappeared-bindingsub-range-bindersoriginoriginal-for-check-syntax对宏编写者而言,disappeared-use属性动得最频繁。所有不出现在宏返回的syntax对象中的 identifier ,都应该被记录 syntax property 的这一项里(除了宏自己的名字的 identifier ,那个是由expander记录到origin属性)。
现在看一下几种常见的情况。
syntax-rules、syntax-case等的pattern里面的 literal identifier ,是disappeared-use属性遗漏的重灾区(截至7.8,case宏仍不能给else的使用画上箭头)。
下面这个程序非常简单,但是foo的使用却画不出箭头:
#lang racket(define-syntaxfoo(syntax-rules()))(define-syntaxbar(syntax-rules(foo)[(_foox)x]))(barfoo1)
所以,syntax-rules是不能自动处理好这个问题的。当需要匹配syntax中的 literal identifier 时,不要用syntax-rules。
(syntax-rules () _ ...)以外的用法都是不恰当的。
先考虑换成syntax-case:
(define-syntax(barstx)(syntax-casestx(foo)[(_foox)#'x]))
这里有一个麻烦的地方,syntax-case不会为pattern中的 literal identifier 引入 pattern variable ,不能直接用 #'foo 访问到用户输入的foo。因此要变通一下:
(define-syntax(barstx)(syntax-casestx()[(_foo-idx)(free-identifier=?#'foo-id#'foo)#'x]))
这里选择用syntax-case的"fender-expr"来对 literal identifier 进行匹配,这样#'foo-id就是用户输入的foo了。
然后是添加disappeared-use:
(define-syntax(barstx)(syntax-casestx()[(_foo-idx)(free-identifier=?#'foo-id#'foo)(syntax-property#'x'disappeared-use(list(syntax-local-introduce#'foo-id)))]))
这里的syntax-local-introduce是必要的,因为宏展开结束反转 scope 的时候不会深入到 syntax property 里面的 identifier 。为了让foo能被正确识别为原始输入的一部分,需要手动用syntax-local-introduce反转 scope 。
另一方面,syntax-parse支持#:track-literals选项,这种情况的处理就非常简单了:
(define-syntax-parserbar#:track-literals[(_(~literalfoo)x)#'x])
可以看出syntax-parse的巨大优势。因此在编写宏时,能用syntax-parse的应该尽量用。
对于模拟单步的宏展开的"pattern expander"(见可扩展的宏),情况要稍微复杂一些。有几种情况:
#lang racket(require(for-syntaxsyntax/apply-transformer)syntax/parse/define)(begin-for-syntax(define(apply-expanderprocstx)(local-apply-transformerprocstx'expression)))(define-syntaxfoo(syntax-rules()))(define-syntax(use-expanderstx)(syntax-casestx()[(_idin)#`(let(#,(apply-expander(syntax-local-value#'id)#'in))(void))]))(define-syntax-parserexpander1#:track-literals[(~literalfoo)#'[x1]])(use-expanderexpander1foo)
这里,expander1的结果没有放在表达式位置,即便添加了#:track-literals,也没有用。
这种情况可以用with-disappeared-uses:
(define-syntax(use-expanderstx)(syntax-casestx()[(_idin)(with-disappeared-uses(record-disappeared-uses#'id)#`(let(#,(apply-expander(syntax-local-value#'id)#'in))(void)))]))(define-syntax-parserexpander1[(~and(~literalfoo)foo-id)(record-disappeared-uses#'foo-id)#'[x1]])
这样,expander1和foo都画上了箭头。
把上面的apply-expander定义换成:
(define(apply-expanderprocstx)(defineintroducer(make-syntax-introducer))(defineintro-stx(introducer(syntax-local-introducestx)))(syntax-local-introduce(introducer(procintro-stx))))
这种旧式的展开方法因为record-disappeared-uses默认的syntax-local-introduce不是上面的introducer,所以画不出foo的箭头。
需要提供正确的introducer,并让record-disappeared-uses不进行syntax-local-introduce:
#lang racket(require(for-syntaxracket/syntax)syntax/parse/define)(begin-for-syntax(definecurrent-introducer(make-parameter#f))(define(current-introducex)((current-introducer)x))(define(apply-expanderprocstx)(defineintroducer(make-syntax-introducer))(defineintro-stx(introducer(syntax-local-introducestx)))(syntax-local-introduce(introducer(parameterize([current-introducerintroducer])(procintro-stx))))))(define-syntaxfoo(syntax-rules()))(define-syntax(use-expanderstx)(syntax-casestx()[(_idin)(with-disappeared-uses(record-disappeared-uses#'id)#`(let(#,(apply-expander(syntax-local-value#'id)#'in))(void)))]))(define-syntax-parserexpander1[(~and(~literalfoo)foo-id)(record-disappeared-uses(current-introduce#'foo-id)#f)#'[x1]])(use-expanderexpander1foo)
如果上面的use-expander不能改,而expander1能改,那就要在expander1里寻找一个表达式位置了:
(define-syntax-parserexpander1[(~andfoo-id(~literalfoo))#:withexpr(syntax-property#'1'disappeared-use(list(syntax-local-introduce#'foo-id)))#'[xexpr]])
若是非local-apply-transformer的情况(例如for),syntax-local-introduce不适用,可以改为用宏延迟 syntax property 的添加:
(define-syntax-parserdisappeared-use[(_x:id...)(syntax-property#'(void)'disappeared-use(mapsyntax-local-introduce(syntax->list#'(x...))))])(define-syntax-parserexpander1[(~andfoo-id(~literalfoo))#'[x(begin(disappeared-usefoo-id)1)]])
local-expand的结果拆出一部分,原有的 syntax property 可能会遗失。struct那样引入名字由多个输入组合而成的定义的情况,需要添加sub-range-binders属性。参考如何使用First Class Internal Definition Context:
(define-syntax-rule(syntax/trackform)(syntax-casethis-syntax()[(head._)(syntax-track-origin#'formthis-syntax#'head)]))]...[(beginform...)#:with(expanded-form...)(stx-maploop#'(form...))(syntax/track(beginexpanded-form...))]...
这里使用了syntax-track-origin来复制原有的 syntax property ,而原来的begin则被添加到origin属性中了。如果要继续添加disappeared-use属性,需要与原来的属性组合,类似于:
(syntax-propertyv'disappeared-use(cons(syntax-local-introduce#'id)(or(syntax-propertyv'disappeared-use)null)))
至于sub-range-binders属性的用法比较简单,可以直接看The Racket Reference,一般用format-id的#:subs?特性即可。
其他情况也有,但由于不常见,这里不展开讨论。
internal-definition-context-track。syntax-parse的xx:id的情况,这里由于没有一个用户提供的xx或id,会需要手动构造带有恰当的源码位置信息的 identifier ,并且添加original-for-check-syntax属性。用这些箭头画一些图案的行为叫做Arrow Art,示例(需要通过右键 -> "Tack/Untack Arrow(s)"固定箭头看到效果):
#lang racket(define-syntax(arrow-artstx)(syntax-casestx()[(_id...)(syntax-property(syntax-property#'(void)'disappeared-use(mapsyntax-local-introduce(syntax->list#'(id...))))'disappeared-binding(mapsyntax-local-introduce(syntax->list#'(id...))))]))(arrow-artaaa)(arrow-artbbbb)