local-expand该怎么用

什么时候需要用local-expand

参数怎么选

局部的完全展开与Expression Context

stop-ids参数选择null 时,context-v一般是'expression。也就是说,局部的完全展开需要 expression context 。而如果当前的(syntax-local-context)不是'expression时,则需要延迟到 expression context

#lang racket

(define-syntax (local stx)
  (syntax-case stx ()
    [(_ e)
     (local-expand #'e 'expression null)]))

(new
 (class object%
   (super-new)
   (define/public (a) 1)
   (local (displayln (a)))))
;;; class: misuse of method (not in application) in: a

这个例子中,local的展开是一个 internal definition context ,意味着环境还没有设置完毕,但却进行了完全展开,因此出现问题。

正确写法:用#%expression延迟到 expression context 再展开

#lang racket

;;;也可以直接用make-expression-transformer
(define-syntax (local stx)
  (syntax-case stx ()
    [(_ e)
     (eq? (syntax-local-context) 'expression)
     (local-expand #'e 'expression null)]
    [form
     #'(#%expression form)]))

(new
 (class object%
   (super-new)
   (define/public (a) 1)
   (local (displayln (a)))))

那么什么时候要用功能类似的syntax-local-expand-expression呢?情况可以分为以下几种

其他的完全展开

一般是在实现新的#%module-begin时使用,此时context-v'module-beginstop-ids一般是(list #'module*)null

部分展开

部分展开的情况就比较混乱了,没有固定的用法,这里只能简单总结一下。

部分展开 vs 完全展开

definition context 做宏展开时,一般要经历两个pass:

  1. 对宏进行部分展开,这一步碰到define-valuesdefine-syntaxes或者其他 Fully Expanded Program 的form就会停下。这一步会建立环境,该 definition context 下的 binding 都是这一步引入的。
  2. 完全展开。

如上面的例子所示的,在1做完全展开会因为环境缺失出现问题。同理,在1时使用identifier-binding也可能得不到准确的结果。在2时试图修改环境也会出现问题,从一些设计中可以看出来:

另一方面,部分展开会把 Fully Expanded Program 的结构也加入stop-ids,这也导致只能展开最外层的syntax。

什么时候要对local-expand的结果用syntax-disarm

在对结果进行操作时,如果只对最外层的名字进行检测不把拆开的结果返回,或者只拆开begin begin-for-syntax #%plain-module-begin define-values define-syntaxes,则不需要syntax-disarm

如果还要再进一步拆开里面的syntax对象,需要先对其使用syntax-disarm,再对返回值使用syntax-rearminspector使用module声明时的inspector,可通过(variable-reference->module-declaration-inspector (#%variable-reference))获得。

补充:8.2.0.4之后,不再需要使用syntax-disarm

使用示例

如果要写这样一个宏(let-box ([x rhs]) body ...):如果xbody里没有被set!过,那么就跟普通的let一样;如果被set!过了,那么x被放到box里,并且所有对x的引用变成unbox ,所有对x的赋值变成set-box!

应该怎么写呢?

  1. x有没有set!过,需要body完全展开之后才能得知,所以需要用local-expand进行完全展开
  2. 需要完全展开,所以延迟到 expression context
  3. 要记录set!的情况,可以把x的名字绑定到一个 set!-transformer ,在里面通知let-box。这个过程可以用 3d syntax 简单地完成。
  4. 因为没有被set!的情况下等于普通的let,可以利用syntax-local-expand-expression进行优化。
  5. set!过了的话,要对展开结果进行修改,因此需要syntax-disarm

终上所述,let-box写出来如下:

#lang racket
(require syntax/parse/define)

(define-syntax-parser let-box
  [(_ ([x:id rhs]) body ...+)
   #:when (eq? (syntax-local-context) 'expression)
   #:do [(define setted? #f)]
   #:with proc (datum->syntax #f (λ () (set! setted? #t)))
   #:do [(define-values (expanded opaque) 
           (syntax-local-expand-expression
            #'(let ([x rhs])
                (let-syntax ([x (make-set!-transformer
                                 (lambda (stx)
                                   (syntax-case stx (set!)
                                     [(set! id v) (begin ('proc) #'(set! x v))]
                                     [id (identifier? #'id)  #'x])))])
                  body ...))))]
   (cond
     [(not setted?) opaque]
     [else
      (define insp
        (variable-reference->module-declaration-inspector
         (#%variable-reference)))
      (syntax-case (syntax-disarm expanded insp) (let-values)
        [(let-values ([(x) rhs]) body ...)
         (syntax-rearm
          #'(let ([x (box rhs)])
              (let-syntax ([x (make-set!-transformer
                               (lambda (stx)
                                 (syntax-case stx (set!)
                                   [(set! id v) #'(set-box! x v)]
                                   [id (identifier? #'id)  #'(unbox x)])))])
                body ...))
          expanded)])])]
  [form
   #'(#%expression form)])

使用如下:

> (let-box ([x 1])
           x)
1
> (let-box ([x 1])
           (set! x 2)
           x)
2
> (syntax->datum (expand '(let-box ([x 1])
                                   x)))
'(#%expression (let-values (((x) '1)) (let-values () (let-values () x))))
> (syntax->datum (expand '(let-box ([x 1])
                                   (set! x 2)
                                   x)))
'(#%expression
  (let-values (((x) (#%app box '1)))
    (let-values ()
      (let-values () (let-values () (let-values () (#%app set-box! x '2) (#%app unbox x)))))))