Racket编程指南——16 宏

16 

宏(macro)是一种语法表,它有一个关联的转换器,它将原有的表展开为现有的表。换句话说,宏是Racket编译器的扩展。racket/baseracket的大部分句法表实际上是宏,展开成一小部分核心结构。

像许多语言一样,Racket提供基于模式的宏,使得简单的转换易于实现并可靠使用。Racket还支持任意在Racket中实现或在Racket中宏展开变体中实现的宏转换器。

(对于自下而上的Racket宏的介绍,你可以参考:《宏的担忧》)

    16.1 基于模式的宏

      16.1.1 define-syntax-rule

      16.1.2 词法范围

      16.1.3 define-syntaxsyntax-rules

      16.1.4 序列的匹配

      16.1.5 标识宏

      16.1.6 set!转化器

      16.1.7 宏生成宏

      16.1.8 展开的例子:按引用调用函数

    16.2 通用宏转换器

      16.2.1 语法对象

      16.2.2 宏转化器程序

      16.2.3 混合模式和表达式:syntax-case

      16.2.4 with-syntaxgenerate-temporaries

      16.2.5 编译和运行时阶段

      16.2.6 一般阶段级别

        16.2.6.1 阶段和绑定

        16.2.6.2 阶段和模块

      16.2.7 语法污染

 

16.1 基于模式的宏

基于模式的宏将任何与模式匹配的代码替换为使用与模式部分匹配的原始语法的一部分的展开。

16.1.1 define-syntax-rule

创建宏的最简单方法是使用define-syntax-rule

(define-syntax-rule pattern template)

作为一个运行的例子,思考这个swap宏,它将交换值存储在两个变量中。可以使用define-syntax-rule实现如下:

宏在这个意义上是“非Racket的”,它涉及到变量上的副作用——但宏的重点是让你添加一些其它语言设计师可能不认可的语法表。

(define-syntax-rule (swap x y)
  (let ([tmp x])
    (set! x y)
    (set! y tmp)))

define-syntax-rule表绑定一个与单个模式匹配的宏。模式必须总是以一个开放的括号开头,后面跟着一个标识,这个标识在这个例子中是swap。在初始的标识之后,其它标识是宏模式变量),可以匹配宏使用中的任何内容。因此,这个宏匹配这个表(swap form1 form2)给任何form1form2

match来说宏模式变量与模式变量是类似的。参见《模式匹配》。

define-syntax-rule中的模式之后是摸板。模板用于替代与模式匹配的表,但模板中的模式变量的每个实例都替换为宏使用模式变量匹配的部分。例如,在

(swap first last)

里,模式变量x匹配firsty匹配last,于是展开成为

(let ([tmp first])
  (set! first last)
  (set! last tmp))

16.1.2 词法范围

假设我们使用swap宏来交换名为tmpother的变量:

(let ([tmp 5]
      [other 6])
  (swap tmp other)
  (list tmp other))

上述表达式的结果应为(6 5)。然而,这种swap的使用的直接展开为

(let ([tmp 5]
      [other 6])
  (let ([tmp tmp])
    (set! tmp other)
    (set! other tmp))
  (list tmp other))

其结果是(5 6)。问题在于,这个直接的展开搞混了上下文中的tmp,那里swap与宏摸板中的tmp被使用。

Racket不会为了swap的上述使用生成直接的展开。相反,它会这样生成内容

(let ([tmp 5]
      [other 6])
  (let ([tmp_1 tmp])
    (set! tmp other)
    (set! other tmp_1))
  (list tmp other))

正确的结果为(6 5)。同样,在示例中

(let ([set! 5]
      [other 6])
  (swap set! other)
  (list set! other))

其展开是

(let ([set!_1 5]
      [other 6])
  (let ([tmp_1 set!_1])
    (set! set!_1 other)
    (set! other tmp_1))
  (list set!_1 other))

因此局部set!绑定不会干扰宏模板引入的赋值。

换句话说,Racket的基于模式的宏自动维护词法范围,所以宏的实现者可以思考宏中的变量引用以及在同样的途径中作为函数和函数调用的宏使用。

16.1.3 define-syntaxsyntax-rules

define-syntax-rule表绑定一个与单一模式匹配的宏,但Racket的宏系统支持从同一标识开始匹配多个模式的转换器。要编写这样的宏,程序员必须使用更通用的define-syntax表以及syntax-rules转换器表:

(define-syntax id
  (syntax-rules (literal-id ...)
    [pattern template]
    ...))

define-syntax-rule表本身就是一个宏,它用一个仅包含一个模式和模板的syntax-rules表展开成define-syntax

例如,假设我们希望一个rotate宏将swap概括为两个或三个标识,从而

(let ([red 1] [green 2] [blue 3])
  (rotate red green)      ; swaps
  (rotate red green blue) ; rotates left
  (list red green blue))

生成(1 3 2)。我们可以使用syntax-rules实现 rotate

(define-syntax rotate
  (syntax-rules ()
    [(rotate a b) (swap a b)]
    [(rotate a b c) (begin
                     (swap a b)
                     (swap b c))]))

表达式(rotate red green)syntax-rules表中的第一个模式相匹配,因此展开到(swap red green)。表达式(rotate red green blue)与第二个模式匹配,所以它展开到(begin (swap red green) (swap green blue))

16.1.4 序列的匹配

一个更好的rotate宏将允许任意数量的标识,而不是只有两个或三个标识。匹配任何数量的标识的rotate的使用,我们需要一个模式表,它有点像克林闭包(Kleene star)。在一个Racket宏模式中,一个闭包(star)被写成...

为了用...实现rotate,我们需要一个基元(base case)来处理单个标识,以及一个归纳案例以处理多个标识:

(define-syntax rotate
  (syntax-rules ()
    [(rotate a) (void)]
    [(rotate a b c ...) (begin
                          (swap a b)
                          (rotate b c ...))]))

当在一种模式中像c这样的模式变量被...跟着的时候,它在模板中必须也被...跟着。模式变量有效地匹配一个零序列或多个表,并在模板中以相同的顺序被替换。

到目前为止,rotate的两种版本都有点效率低下,因为成对交换总是将第一个变量的值移动到序列中的每个变量,直到达到最后一个变量为止。更有效的rotate将第一个值直接移动到最后一个变量。我们可以用...模式使用辅助宏去实现更有效的变体:

(define-syntax rotate
  (syntax-rules ()
    [(rotate a c ...)
     (shift-to (c ... a) (a c ...))]))
(define-syntax shift-to
  (syntax-rules ()
    [(shift-to (from0 from ...) (to0 to ...))
     (let ([tmp from0])
       (set! to from) ...
       (set! to0 tmp))]))

shift-to宏里,模板里的...后跟着(set! to from),它导致(set! to from)表达式被重复多次,来使用tofrom中匹配的每个标识序列。(tofrom匹配的数量必须相同,否则这个宏展开就会因一个错误而失败。)

16.1.5 标识宏

根据我们的宏定义,swaprotate标识必须在开括号之后使用,否则会报告语法错误:

> (+ swap 3)

eval:2:0: swap: bad syntax

  in: swap

一个标识宏(identifier macro)是一个模式匹配宏,当它被自己使用时不使用括号。例如,我们可以把val定义为一个展开到(get-val)的标识宏,这样(+ val 3)将展开到(+ (get-val) 3)

> (define-syntax val
    (lambda (stx)
      (syntax-case stx ()
        [val (identifier? (syntax val)) (syntax (get-val))])))
> (define-values (get-val put-val!)
    (let ([private-val 0])
      (values (lambda () private-val)
              (lambda (v) (set! private-val v)))))
> val

0

> (+ val 3)

3

val宏使用syntax-case,它可以定义更强大的宏,这个在《混合模式和表达式:syntax-case》中讲解。现在,知道定义宏是必要的就足够了,在lambda中使用了syntax-case,而且其模板必须用明确的syntax构造器包装。最后,syntax-case从句可以指定模式后面的附加保护条件。

我们的val宏使用identifier?条件确保在括号中val一定不使用。反之,宏引一个发语法错误:

> (val)

eval:8:0: val: bad syntax

  in: (val)

16.1.6 set!转化器

使用上面的val宏,我们仍然必须调用put-val!更改存储值。然而,直接在val上使用set!会更方便。当val用于set!时援引宏,我们用make-set!-transformer创建一个赋值转换器。我们还必须声明set!作为syntax-case原语列表中的原语。

> (define-syntax val2
    (make-set!-transformer
     (lambda (stx)
       (syntax-case stx (set!)
         [val2 (identifier? (syntax val2)) (syntax (get-val))]
         [(set! val2 e) (syntax (put-val! e))]))))
> val2

0

> (+ val2 3)

3

> (set! val2 10)
> val2

10

16.1.7 宏生成宏

假设我们有许多标识,像valval2,我们想重定向给访问函数和变位函数,像get-valput-val!。我们希望可以这样写:

(define-get/put-id val get-val put-val!)

自然地,我们可以实现define-get/put-id为一个宏:

> (define-syntax-rule (define-get/put-id id get put!)
    (define-syntax id
      (make-set!-transformer
       (lambda (stx)
         (syntax-case stx (set!)
           [id (identifier? (syntax id)) (syntax (get))]
           [(set! id e) (syntax (put! e))])))))
> (define-get/put-id val3 get-val put-val!)
> (set! val3 11)
> val3

11

define-get/put-id是一个宏生成宏

16.1.8 展开的例子:按引用调用函数

我们可以使用模式匹配宏将一个表添加到Racket中,以定义一阶按引用调用函数。当通过按引用调用函数主体转变它的正式参数,这个转变应用到变量,它作为函数调用中的实参提供。

例如,如果define-cbr类似于define,除了定义按引用调用函数,也可以这样

(define-cbr (f a b)
  (swap a b))
(let ([x 1] [y 2])
  (f x y)
  (list x y))

生成(2 1)

我们会通过有函数调用支持的对参数的访问器和转换器执行按引用调用函数,而不是直接提供参数值。特别是,对于上面的函数f,我们将生成

(define (do-f get-a get-b put-a! put-b!)
  (define-get/put-id a get-a put-a!)
  (define-get/put-id b get-b put-b!)
  (swap a b))

并将函数调用(f x y)重定向到

(do-f (lambda () x)
      (lambda () y)
      (lambda (v) (set! x v))
      (lambda (v) (set! y v)))

显然,define-cbr是一个宏生成宏,它绑定f到一个宏,这个宏展开到do-f的调用,换句话说,(define-cbr (f a b) (swap a b))需要去生成这个定义

(define-syntax f
  (syntax-rules ()
    [(id actual ...)
     (do-f (lambda () actual)
           ...
           (lambda (v)
             (set! actual v))
           ...)]))

同时,define-cbr需要使用f本体去定义do-f,第二部分略微更复杂些,所以我们把它的大部委托给一个define-for-cbr辅助模块,它可以让我们足够简单地编写define-cbr

(define-syntax-rule (define-cbr (id arg ...) body)
  (begin
    (define-syntax id
      (syntax-rules ()
        [(id actual (... ...))
         (do-f (lambda () actual)
               (... ...)
               (lambda (v)
                 (set! actual v))
               (... ...))]))
    (define-for-cbr do-f (arg ...)
      () ; 下面的解释……
      body)))

我们剩下的任务是定义define-for-cbr以便它转换

(define-for-cbr do-f (a b) () (swap a b))

给上边的这个函数定义do-f两个功能定义。大部分的工作是为每个参数生成一个define-get/put-id声明,ab,并把它们放在本体之前。通常,对于在模式和模板中的...来说,那是很容易的任务,但这次这里有一个捕获:我们需要生成这些名字get-aput-a!以及get-bput-b!,这个模式语言没有办法提供基于现有标识的综合标识。

事实证明,词法范围给了我们解决这个问题的方法。诀窍是为函数中的每个参数迭代一次define-for-cbr的展开,这就是为什么define-for-cbr用一个在参数列表后面明显无效的()作为开始的原因。除了要处理的参数外,我们还需要跟踪迄今为止所看到的所有参数以及为每个参数生成的getput名称。在处理完所有的标识之后,我们就拥有了所有需要的名称。

这是define-for-cbr的定义:

(define-syntax define-for-cbr
  (syntax-rules ()
    [(define-for-cbr do-f (id0 id ...)
       (gens ...) body)
     (define-for-cbr do-f (id ...)
       (gens ... (id0 get put)) body)]
    [(define-for-cbr do-f ()
       ((id get put) ...) body)
     (define (do-f get ... put ...)
       (define-get/put-id id get put) ...
       body)]))

逐步地,展开如下:

(define-for-cbr do-f (a b)
  () (swap a b))
=> (define-for-cbr do-f (b)
     ([a get_1 put_1]) (swap a b))
=> (define-for-cbr do-f ()
     ([a get_1 put_1] [b get_2 put_2]) (swap a b))
=> (define (do-f get_1 get_2 put_1 put_2)
     (define-get/put-id a get_1 put_1)
     (define-get/put-id b get_2 put_2)
     (swap a b))

get_1get_2put_1put_2上的“下标(subscript)”通过宏展开插入到保留词法范围,因为每次迭代define-for-cbr生成的get不应绑定不同迭代生成的get。换句话说,我们本质上是在欺骗宏展开器以为我们生成新名字,但该技术说明了基于模式的宏具有自动词法范围的惊人力量。

最后这个表达式最终展开为

(define (do-f get_1 get_2 put_1 put_2)
  (let ([tmp (get_1)])
    (put_1 (get_2))
    (put_2 tmp)))

它实现了按名称调用函数f

接下来,总结一下,我们可以只用三个基于模式的小巧宏添加按引用调用函数到Racket中:define-cbrdefine-for-cbrdefine-get/put-id

 

16.2 通用宏转换器

define-syntax表为标识创建一个转换器绑定(transformer binding),这是一个可以在编译时使用的绑定,同时展开表达式以在运行时进行求值。与转换器绑定相关联的编译时值可以是任何东西;如果它是一个参数的过程,则绑定用作宏,而过程是 宏转换器(macro transformer)。

    16.2.1 语法对象

    16.2.2 宏转化器程序

    16.2.3 混合模式和表达式:syntax-case

    16.2.4 with-syntax和generate-temporaries

    16.2.5 编译和运行时阶段

    16.2.6 一般阶段级别

      16.2.6.1 阶段和绑定

      16.2.6.2 阶段和模块

    16.2.7 语法污染

 

16.2.1 语法对象

宏转换器(即源和替换表)的输入和输出被表示为语法对象(syntax object)。语法对象包含符号、列表和常量值(如数字),它们基本上与表达式的quote表相对应。例如,表达式描述为(+ 1 2)包含符号'+和数字1和2,都在列表中。除了引用的内容之外,语法对象还将源位置和词汇绑定信息与表的每个部分关联起来。在报告语法错误时使用源位置信息(例如),词汇绑定信息允许宏系统维护词法范围。为了适应这种额外的信息,表达式(+ 1 2)的表示不仅是'(+ 1 2),而是将'(+ 1 2)打包成了语法对象。

要创建字面语法对象,使用syntax表:

> (syntax (+ 1 2))

#<syntax:eval:1:0 (+ 1 2)>

在同样的方式,'省略了quote,#'省略了syntax:

> #'(+ 1 2)

#<syntax:eval:1:0 (+ 1 2)>

只包含符号的语法对象是标识语法对象(identifier syntax object)。Racket提供了一些特定于标识语法对象的附加操作,包括identifier?操作以检查标识。最值得注意的是,free-identifier=?确定两个标识是否引用相同的绑定:

> (identifier? #'car)

#t

> (identifier? #'(+ 1 2))

#f

> (free-identifier=? #'car #'cdr)

#f

> (free-identifier=? #'car #'car)

#t

> (require (only-in racket/base [car also-car]))
> (free-identifier=? #'car #'also-car)

#t

要在语法对象中看到列表、符号、数字等,使用syntax->datum:

> (syntax->datum #'(+ 1 2))

'(+ 1 2)

syntax-e函数类似于syntax->datum,但它将源位置和词汇上下文信息单层解包,留下将自己的信息打包为语法对象的子表单:

> (syntax-e #'(+ 1 2))

'(#<syntax:eval:1:0 +> #<syntax:eval:1:0 1> #<syntax:eval:1:0 2>)

syntax-e函数总是在通过符号、数值和其它字面值所表示的子表周围留下语法对象打包器。它唯一解包额外子表是当解包一个序对时,在这种情况下,序对的cdr可以递归解包,取决于语法对象的构造方式。

当然,syntax->datum的反义词是datum->syntax。除了像'(+ 1 2)这样的数据外,datum->syntax还需要一个现有的语法对象来贡献它的词汇上下文,并且可以选择另一个语法对象来贡献它的源位置:

> (datum->syntax #'lex
                 '(+ 1 2)
                 #'srcloc)

#<syntax:eval:1:0 (+ 1 2)>

在上面的例子中,#'lex的词法上下文用于新的语法对象,而#'srcloc的源位置则被使用。

当datum->syntax的第二个(即,“datum”)参数包含语法对象时,这些语法对象将原封不动地保存在结果中。那就是,用syntax-e解构的结果最终产生了给予datum->syntax的这个语法对象。

 

16.2.2 宏转化器程序

一个参数的任何过程都可以是宏转换器。事实证明,syntax-rules表是一个展开为过程表的宏。例如,如果直接对syntax-rules表求值(而不是放在define-syntax表的右侧),结果就是一个过程:

> (syntax-rules () [(nothing) something])

#<procedure>

可以使用lambda直接编写自己的宏转换器过程,而不是使用syntax-rules。过程的参数是表示源表的语法对象,过程的结果必须是表示替换表的语法对象

> (define-syntax self-as-string
    (lambda (stx)
      (datum->syntax stx
                     (format "~s" (syntax->datum stx)))))
> (self-as-string (+ 1 2))

"(self-as-string (+ 1 2))"

传递给宏转换器的源表表示在应用程序位置(即在启动表达式的括号之后)使用其标识的表达式,或者,如果它用作表达式位置而不是应用程序位置,则它本身表示标识。syntax-rules产生的过程如果其参数本身对应于标识的使用,则会引发语法错误,这就是为什么syntax-rules不实现一个标识宏的原因。

> (self-as-string (+ 1 2))

"(self-as-string (+ 1 2))"

> self-as-string

"self-as-string"

define-syntax表支持与define的函数一样的快捷语法,因此下面的self-as-string定义等同于显式地使用lambda的那个定义:

> (define-syntax (self-as-string stx)
    (datum->syntax stx
                   (format "~s" (syntax->datum stx))))
> (self-as-string (+ 1 2))

"(self-as-string (+ 1 2))"

 

16.2.3 混合模式和表达式:syntax-case

通过syntax-rules生成的程序在内部使用syntax-e来解构语法对象,并使用datum->syntax来构造结果。syntax-rules表没有提供一种方法来从模式匹配和模板构建模式中跳转到任意的Racket表达式中。

syntax-case表允许混合模式匹配、模板构造和任意表达式:

(syntax-case stx-expr (literal-id ...)
  [pattern expr]
  ...)

与syntax-rules不同,syntax-case表不产生过程。相反,它从一个stx-expr表达式决定的语法对象来匹配pattern。另外,每个syntax-case从句有一个pattern和一个expr,而不是pattern和template。在expr里,syntax表——通常用#'缩写——转换为模板构造方式;如果一个从句的expr以#'开始,那么我们就会有一个类似于syntax-rules的表:

> (syntax->datum
   (syntax-case #'(+ 1 2) ()
    [(op n1 n2) #'(- n1 n2)]))

'(- 1 2)

我们可以使用syntax-case来编写swap宏,而不是使用define-syntax-rule或syntax-rules:

(define-syntax (swap stx)
  (syntax-case stx ()
    [(swap x y) #'(let ([tmp x])
                    (set! x y)
                    (set! y tmp))]))

使用syntax-case的一个优点是,我们可以给swap提供更好的错误报告。例如,用swap的define-syntax-rule定义,之后(swap x 2)在set!条件中产生了语法错误,因为2不是一个标识。我们可以改进swap的syntax-case实现来显式地检查子表:

(define-syntax (swap stx)
  (syntax-case stx ()
    [(swap x y)
     (if (and (identifier? #'x)
              (identifier? #'y))
         #'(let ([tmp x])
             (set! x y)
             (set! y tmp))
         (raise-syntax-error #f
                             "not an identifier"
                             stx
                             (if (identifier? #'x)
                                 #'y
                                 #'x)))]))

通过这个定义,(swap x 2)提供了一个源自swap而不是set!的语法错误。

在上述swap的定义里,#'x和#'y是模板,即使它们不是作为宏转换器的结果。这个例子说明了如何使用模板来访问输入语法的片段,在这种情况下可以检查片段的表。同时,#'x或#'y的匹配项用于调用raise-syntax-error,于是语法错误信息可以直接指到非标识的源位置。

 

16.2.4 with-syntax和generate-temporaries

既然syntax-case允许我们用任意的Racket表达式计算,我们可以更简单地解决我们在编写define-for-cbr(参见《展开的例子:按引用调用函数》)中的一个问题,在这里我们需要根据序列id ...生成一组名称:

(define-syntax (define-for-cbr stx)
  (syntax-case stx ()
    [(_ do-f (id ...) body)
     ....
       #'(define (do-f get ... put ...)
           (define-get/put-id id get put) ...
           body) ....]))

代替上面的....我们需要绑定get ...和put ...到生成标识的列表。我们不能使用let绑定get和put,因为我们需要绑定那个计数作为模式变量,而不是普通的局部变量。with-syntax表允许我们绑定模式变量:

(define-syntax (define-for-cbr stx)
  (syntax-case stx ()
    [(_ do-f (id ...) body)
     (with-syntax ([(get ...) ....]
                   [(put ...) ....])
       #'(define (do-f get ... put ...)
           (define-get/put-id id get put) ...
           body))]))

现在我们需要一个表达式来代替....生成尽可能多的标识符,因为在原始模式中有id。由于这是一个常见任务,Racket提供了一个辅助函数,generate-temporaries,它接受一系列标识并返回一系列生成的标识:

(define-syntax (define-for-cbr stx)
  (syntax-case stx ()
    [(_ do-f (id ...) body)
     (with-syntax ([(get ...) (generate-temporaries #'(id ...))]
                   [(put ...) (generate-temporaries #'(id ...))])
       #'(define (do-f get ... put ...)
           (define-get/put-id id get put) ...
           body))]))

这种生成标识的方法通常比诱使宏展开器使用纯粹基于模式的宏生成名称更容易理解。

一般来说,with-syntax绑定左边是一个模式,就像在syntax-case中一样。事实上,with-syntax表只是一个部分由内向外翻转的syntax-case表。

 

16.2.5 编译和运行时阶段

随着宏集变得越来越复杂,你可能需要编写你自己的辅助函数,像generate-temporaries。例如,提供良好的语法错误消息,swaprotatedefine-cbr都应该检查源表中的某些子表是否是标识。我们可以使用check-ids函数在任何地方执行此检查:

(define-syntax (swap stx)
  (syntax-case stx ()
    [(swap x y) (begin
                  (check-ids stx #'(x y))
                  #'(let ([tmp x])
                      (set! x y)
                      (set! y tmp)))]))
(define-syntax (rotate stx)
  (syntax-case stx ()
    [(rotate a c ...)
     (begin
       (check-ids stx #'(a c ...))
       #'(shift-to (c ... a) (a c ...)))]))

check-ids函数可以使用syntax->list函数包装列表的语法对象转换成语法对象列表:

(define (check-ids stx forms)
  (for-each
   (lambda (form)
     (unless (identifier? form)
       (raise-syntax-error #f
                           "not an identifier"
                           stx
                           form)))
   (syntax->list forms)))

然而,如果以这种方式定义swapcheck-ids,则它不起作用:

> (let ([a 1] [b 2]) (swap a b))

check-ids: undefined;

 cannot reference an identifier before its definition

  in module: top-level

问题是check-ids被定义为一个运行时表达式,但是swap试图在编译时使用它。在交互模式中,编译时和运行时是交错的,但它们不会在模块主体内交错,而且它们也不会在预编译的模块之间交错。为了帮助所有这些模式一致地对待代码,Racket将不同阶段的绑定空间分隔开来。

要定义可在编译时被引用的check-ids函数,使用begin-for-syntax

(begin-for-syntax
  (define (check-ids stx forms)
    (for-each
     (lambda (form)
       (unless (identifier? form)
         (raise-syntax-error #f
                             "not an identifier"
                             stx
                             form)))
     (syntax->list forms))))

使用此语法定义,swap就可以运行了:

> (let ([a 1] [b 2]) (swap a b) (list a b))

'(2 1)

> (swap a 1)

eval:13:0: swap: not an identifier

  at: 1

  in: (swap a 1)

当将程序组织成模块时,你也许希望将辅助函数放在一个模块中,以供驻留在其它模块上的宏使用。在这种情况下,你可以使用define编写辅助函数:

"utils.rkt"

#lang racket
(provide check-ids)
(define (check-ids stx forms)
  (for-each
   (lambda (form)
     (unless (identifier? form)
       (raise-syntax-error #f
                           "not an identifier"
                           stx
                           form)))
   (syntax->list forms)))

然后,在实现宏模块中,使用(require (for-syntax "utils.rkt"))代替(require "utils.rkt")导入辅助函数:

#lang racket
(require (for-syntax "utils.rkt"))
(define-syntax (swap stx)
  (syntax-case stx ()
    [(swap x y) (begin
                  (check-ids stx #'(x y))
                  #'(let ([tmp x])
                      (set! x y)
                      (set! y tmp)))]))

由于模块是单独编译的,不能有循环依赖项,因此可以在编译实现swap模块之前编译"utils.rkt"模块的运行时主体。因此,只要(require (for-syntax ....))将"utils.rkt"中的运行时定义显式转换为编译时,就可以使用它们来实现swap

racket模块提供了syntax-casegenerate-temporarieslambdaif以及更多以用在运行时阶段和编译时阶段。这就是为什么我们既可以直接在racket的REPL中也可以在一个define-syntax表的右端使用syntax-case的原因。

相反,racket/base模块只在运行时阶段导出这些绑定。如果你更改了上面定义swap的模块,使其使用racket/base语言而不是racket,那么它不再工作。添加(require (for-syntax racket/base))导入syntax-case和更多内容进入编译时阶段,以便模块再次工作。

假设define-syntax用于在define-syntax表的右侧定义一个本地宏。在这种情况下,内部define-syntax的右侧位于元编译阶段级别(meta-compile phase level),也称为阶段级别2(phase level 2)。要将syntax-case导入到该阶段级别,你必须使用(require (for-syntax (for-syntax racket/base))),或者等效地使用(require (for-meta 2 racket/base))。例如,

#lang racket/base
(require  ;; This provides the bindings for the definition
          ;; of shell-game.
          ;;这为shell-game的定义提供了绑定。
          (for-syntax racket/base)
          ;; And this for the definition of
          ;; swap.
          ;;这是互换的定义。
          (for-syntax (for-syntax racket/base)))
(define-syntax (shell-game stx)
  (define-syntax (swap stx)
    (syntax-case stx ()
      [(_ a b)
       #'(let ([tmp a])
           (set! a b)
           (set! b tmp))]))
  (syntax-case stx ()
    [(_ a b c)
     (let ([a #'a] [b #'b] [c #'c])
       (when (= 0 (random 2)) (swap a b))
       (when (= 0 (random 2)) (swap b c))
       (when (= 0 (random 2)) (swap a c))
       #`(list #,a #,b #,c))]))
(shell-game 3 4 5)
(shell-game 3 4 5)
(shell-game 3 4 5)

反向阶段级别也存在。如果宏使用导入的for-syntax辅助函数,并且辅助函数返回由syntax生成的语法对象常量,那么语法中的标识将需要在阶段级别-1(phase level -1),也称为模板阶段级别(template phase level)进行绑定,以便在运行时阶段级别相对于定义宏的模块进行绑定。

例如,在下面的例子中没有语法变换器的swap-stx的辅助函数——它只是一个普通的函数——但它产生的语法对象得到拼接成shell-game的结果。因此,它包含的子模块需要在shell-game第1阶段用(require (for-syntax 'helper))导入。

但从swap-stx的角度,当shell-game返回的语法被求值时,其结果最终在第-1阶段求值。换句话说,一个负向阶段级别是一个从正方向来看相反的阶段级别:shell-game的第1阶段是swap-stx的第0阶段,所以shell-game的第0阶段是swap-stx的的-1阶段。这就是为什么这个例子不起作用的原因——'helper子模块在第-1阶段没有绑定。

#lang racket/base
(require (for-syntax racket/base))
(module helper racket/base
  (provide swap-stx)
  (define (swap-stx a-stx b-stx)
    #`(let ([tmp #,a-stx])
          (set! #,a-stx #,b-stx)
          (set! #,b-stx tmp))))
(require (for-syntax 'helper))
(define-syntax (shell-game stx)
  (syntax-case stx ()
    [(_ a b c)
     #`(begin
         #,(swap-stx #'a #'b)
         #,(swap-stx #'b #'c)
         #,(swap-stx #'a #'c)
         (list a b c))]))
(define x 3)
(define y 4)
(define z 5)
(shell-game x y z)

为修复这个例子,我们添加(require (for-template racket/base))'helper子模块。

#lang racket/base
(require (for-syntax racket/base))
(module helper racket/base
  (require (for-template racket/base)) ; binds `let` and `set!` at phase -1
                                       ;在第-1阶段绑定“let”和“set!”
  (provide swap-stx)
  (define (swap-stx a-stx b-stx)
    #`(let ([tmp #,a-stx])
          (set! #,a-stx #,b-stx)
          (set! #,b-stx tmp))))
(require (for-syntax 'helper))
(define-syntax (shell-game stx)
  (syntax-case stx ()
    [(_ a b c)
     #`(begin
         #,(swap-stx #'a #'b)
         #,(swap-stx #'b #'c)
         #,(swap-stx #'a #'c)
         (list a b c))]))
(define x 3)
(define y 4)
(define z 5)
(shell-game x y z)
(shell-game x y z)
(shell-game x y z)

 

16.2.6 一般阶段级别

阶段(phase)可以被看作是在进程的管道中分离出计算的方法,其中一个进程产生下一个进程使用的代码。(例如,由预处理器进程、编译器和汇编程序组成的管道)。

设想为此启动两个Racket过程。如果忽略套接字和文件等进程间通信通道,进程将无法共享从一个进程的标准输出管道传输到另一个进程标准输入的文本以外的任何内容。类似地,Racket有效地允许一个模块的多个调用存在于同一个进程中,但按阶段分开。Racket强制执行这种阶段的分离,在这种情况下,不同的阶段除了通过宏展开协议之外,不能以任何方式进行通信,其中一个阶段的输出是下一个阶段使用的代码。

16.2.6.1 阶段和绑定

标识的每个绑定都存在于特定阶段。绑定与其阶段之间的链接由整数阶段级别(phase-level)表示。阶段级别0是用于“普通”(或“运行时”)定义的阶段,因此

(define age 5)

为age添加绑定到阶段级别0中。标识age可以用begin-for-syntax在更高的阶段级别定义:

(begin-for-syntax
  (define age 5))

使用单个begin-for-syntax包装器,age在阶段级别1定义。我们可以容易地在同一个模块或顶级命名空间中混合这两个定义,并且在不同的阶段级别上定义的两个age之间没有冲突:

> (define age 3)
> (begin-for-syntax
    (define age 9))

在阶段级别0的age绑定值为3,在阶段级别1的age绑定值为9。

语法对象将绑定信息捕获为一级值。因此,

#'age

是一个表示age绑定的语法对象,但由于有两个age(一个在阶段级别0,一个在阶段级别1),它捕获的是哪一个?事实上,Racket为#'age注入了所有阶段级别的词汇信息,所以答案是#'age同时捕捉了这两者。

#'age捕获的age的相关绑定是在最终使用#'age时确定的。例如,我们将#'age绑定到一个模式变量,以便在模板中使用它,然后eval对模板求值:我们在这里使用eval演示阶段,但请参见《反射和动态求值》了解有关eval的警告。

> (eval (with-syntax ([age #'age])
          #'(displayln age)))

3

结果是3,因为age用于0阶段级别。我们可以在begin-for-syntax内使用age再试一次:

> (eval (with-syntax ([age #'age])
          #'(begin-for-syntax
              (displayln age))))

9

在这种情况下,答案是9,因为我们在阶段级别1使用age,而不是0(即, begin-for-syntax在阶段级别1求值表达式)。所以,你可以看到我们从相同的语法对象开始#'age开始,我们可以用两种不同的方法使用它:在阶段级别0和在阶段级别1。

语法对象从第一次存在时起就具有词法上下文。模块提供的语法对象保留其词法上下文,因此它引用源模块上下文中的绑定,而不是其使用上下文。以下示例在阶段级别0定义了button,并将其绑定到0,而see-button则绑定了模块a中的button的语法对象:

> (module a racket
    (define button 0)
    (provide (for-syntax see-button))
  
    ; 为什么不使用(define see-button #'button)? 我们稍后解释。
    (define-for-syntax see-button #'button))
> (module b racket
    (require 'a)
    (define button 8)
    (define-syntax (m stx)
      see-button)
    (m))
> (require 'b)

0

在m宏的结果是see-button的值,它是#'button,带有模块a模块的词汇上下文。即使在b中有另一个button,第二个button不会混淆Racket,因为#'button的词汇上下文(绑定到see-button的值)是a。

请注意,通过define-for-syntax定义see-button,它绑定在阶段级别1。阶段级别1是必须的,因为m是一个宏,所以它的主体执行的阶段高于其定义的上下文。由于m是在阶段级别0定义的,因此其主体处于阶段级别1,所以由主体引用的任何绑定都必须在阶段级别1。

16.2.6.2 阶段和模块

一个阶段级别(phase level)是一个模块相关概念。当通过require从另一个模块导入时,Racket允许我们将导入的绑定变换为与原始绑定不同的阶段级别:

(require "a.rkt")                ; 不带阶段变换的导入
(require (for-syntax "a.rkt"))   ; 通过+1变换阶段
(require (for-template "a.rkt")) ; 通过-1变换阶段
(require (for-meta 5 "a.rkt"))   ; 通过+5变换阶段

也就是说,在require中使用for-syntax意味着来自该模块的所有绑定的阶段级别都会增加。在阶段级别0为define并用for-syntax导入的绑定成为阶段级别1的绑定:

> (module c racket
    (define x 0) ; 在阶段级别0定义
    (provide x))
> (module d racket
    (require (for-syntax 'c))
    ; 在阶段级别1的绑定,而不是0:
    #'x)

让我们看看如果我们尝试在阶段级别0为#'button语法对象创建绑定会发生了什么:

> (define button 0)
> (define see-button #'button)

现在在第阶段0中定义了button和see-button。#'button的词汇上下文会知道在阶段0有一个绑定。事实上,如果我们尝试对see-button进行eval,似乎一切都很顺利:

> (eval see-button)

0

现在,让我们在宏中使用see-button:

> (define-syntax (m stx)
    see-button)
> (m)

see-button: undefined;

 cannot reference an identifier before its definition

  in module: top-level

显然,see-button在阶段1没有定义,因此我们不能在宏主体内引用它。让我们尝试在另一个模块中使用see-button,方法是将将按钮(button)定义放在模块中,并在阶段级别1导入它。那么,我们将在阶段级别1获得see-button:

> (module a racket
    (define button 0)
    (define see-button #'button)
    (provide see-button))
> (module b racket
    (require (for-syntax 'a)) ; 在阶段级别1获得see-button
    (define-syntax (m stx)
      see-button)
    (m))

eval:1:0: button: unbound identifier;

 also, no #%top syntax transformer is bound

  in: button

Racket说button现在已解除绑定!当在阶段级别1导入a时,我们有以下绑定:

button     在阶段级别1
see-button 在阶段级别1

因此,宏m能够在阶段级别1看到see-button的绑定并将返回#'button语法对象,它指的是在阶段级别1的button绑定。但是m的使用是在阶段级别0,在b的阶段级别0没有button。这就是为什么see-button需要在阶段级别1,就像在原来的a中一样。那么,在原来的b中,我们有以下绑定:

button     在阶段级别0
see-button 在阶段级别1

在这种情况下,我们可以在宏中使用see-button,因为see-button是在阶段级别1绑定的。当宏展开时,它将引用在阶段级别0的button绑定。

用(define see-button #'button)定义see-button本身没有错;它取决于我们打算如何使用see-button。例如,我们可以安排m合理地使用see-button,因为它使用begin-for-syntax将其放在了阶段级别1的上下文中:

> (module a racket
    (define button 0)
    (define see-button #'button)
    (provide see-button))
> (module b racket
    (require (for-syntax 'a))
    (define-syntax (m stx)
      (with-syntax ([x see-button])
        #'(begin-for-syntax
            (displayln x))))
    (m))

0

在这种情况下,模块b在阶段级别1上同时绑定了button和see-button。宏的展开是

(begin-for-syntax
  (displayln button))

这是可行的,因为button是在阶段级别1绑定的。

现在,你可以通过在阶段等级0和阶段等级1中导入a来欺骗阶段系统。然后,你将拥有以下绑定

button     在阶段级别0
see-button 在阶段级别0
button     在阶段级别1
see-button 在阶段级别1

现在,你可能希望宏中的see-button可以工作,但它没有:

> (module a racket
    (define button 0)
    (define see-button #'button)
    (provide see-button))
> (module b racket
    (require 'a
             (for-syntax 'a))
    (define-syntax (m stx)
      see-button)
    (m))

eval:1:0: button: unbound identifier;

 also, no #%top syntax transformer is bound

  in: button

宏m中的see-button来自(for-syntax 'a)导入。要使宏m工作,需要在阶段0绑定button。这种绑定是存在的——它由(require 'a)表明。然而,(require 'a)和(require (for-syntax 'a))是同一模块的不同实例化。阶段1的see-button仅指第1阶段的button,而不是在阶段0从不同实例化绑定的button,即使来自同一个源模块。

实例化之间的这种阶段级别不匹配可以用syntax-shift-phase-level修复。回想一下,像#'button这样的语法对象在所有阶段级别捕获词汇信息。这里的问题是,see-button在阶段1调用,但需要返回一个可以在阶段0进行求值的语法对象。默认情况下,see-button在同一阶段级别绑定到#'button。但是,使用syntax-shift-phase-level,我们可以使see-button在不同的相对阶段级别上引用#'button。在这种情况下,我们使用-1的变换使阶段1的see-button指向阶段0的#'button。(由于阶段变换发生在每一个级别,它也使阶段0的see-button指向阶段-1的#'button)

请注意,syntax-shift-phase-level仅仅创建了一个跨阶段的引用。为了使该引用有效,我们仍然需要在两个阶段实例化模块,以便引用及其目标具有可用的绑定。因此,在模块'b中,我们仍然在阶段0和阶段1导入模块'a——使用(require 'a (for-syntax 'a))——所以我们有一个阶段1绑定用于see-button,一个阶段0绑定用于button。现在宏m就将起作用了。

> (module a racket
    (define button 0)
    (define see-button (syntax-shift-phase-level #'button -1))
    (provide see-button))
> (module b racket
    (require 'a (for-syntax 'a))
    (define-syntax (m stx)
      see-button)
    (m))
> (require 'b)

0

顺便问一下,在阶段0绑定的see-button会发生什么变化?它的#'button绑定也同样转移到阶段-1。由于button本身在阶段-1没有绑定,如果我们试图在阶段0对see-button求值,我们会得到一个错误。换句话说,我们还没有永久地解决错配问题——我们只是把它转移到一个不太麻烦的位置。

> (module a racket
    (define button 0)
    (define see-button (syntax-shift-phase-level #'button -1))
    (provide see-button))
> (module b racket
    (require 'a (for-syntax 'a))
    (define-syntax (m stx)
      see-button)
    (m))
> (module b2 racket
    (require 'a)
    (eval see-button))
> (require 'b2)

button: undefined;

 cannot reference an identifier before its definition

  in module: top-level

当宏试图匹配字面绑定时——使用syntax-case或syntax-parse,也会出现上述不匹配。

> (module x racket
    (require (for-syntax syntax/parse)
             (for-template racket/base))
  
    (provide (all-defined-out))
  
    (define button 0)
    (define (make) #'button)
    (define-syntax (process stx)
      (define-literal-set locals (button))
      (syntax-parse stx
        [(_ (n (~literal button))) #'#''ok])))
> (module y racket
    (require (for-meta 1 'x)
             (for-meta 2 'x racket/base))
  
    (begin-for-syntax
      (define-syntax (m stx)
        (with-syntax ([out (make)])
          #'(process (0 out)))))
  
    (define-syntax (p stx)
      (m))
  
    (p))

eval:2:0: process: expected the identifier `button'

  at: button

  in: (process (0 button))

在这个例子中,make在在阶段级别2的y中使用,它返回#'button语法对象——它是指在x中的阶段级别0绑定的button,以及在(for-meta 2 'x)中的y内阶段级别2绑定的。process宏是在阶段级别1从(for-meta 1 'x)导入的,它知道button应该绑定在阶段级别1。当syntax-parse在process中执行时,它正在寻找在阶段级别1绑定的button,但它只看到阶段级别2绑定,因此不匹配。

为了修正这个例子,我们可以在相对于x的阶段级别1中提供make,然后在阶段级别1在y中导入它:

> (module x racket
    (require (for-syntax syntax/parse)
             (for-template racket/base))
  
    (provide (all-defined-out))
  
    (define button 0)
  
    (provide (for-syntax make))
    (define-for-syntax (make) #'button)
    (define-syntax (process stx)
      (define-literal-set locals (button))
      (syntax-parse stx
        [(_ (n (~literal button))) #'#''ok])))
> (module y racket
    (require (for-meta 1 'x)
             (for-meta 2 racket/base))
  
    (begin-for-syntax
      (define-syntax (m stx)
        (with-syntax ([out (make)])
          #'(process (0 out)))))
  
    (define-syntax (p stx)
      (m))
  
    (p))
> (require 'y)

'ok

 

16.2.7 语法污染

宏的使用可以展开为未从绑定宏的模块导出的标识的使用。一般来说,这样的标识不能从展开表达式中提取出来并在不同的上下文中使用,因为在不同上下文中使用标识可能会破坏宏模块的不变量。

例如,下面的模块导出一个宏go,它展开为使用unchecked-go

"m.rkt"

#lang racket
(provide go)
(define (unchecked-go n x)
  ; 为了避免灾难,n必须是数字
  (+ n 17))
(define-syntax (go stx)
  (syntax-case stx ()
   [(_ x)
    #'(unchecked-go 8 x)]))

如果从(go 'a)展开中解析了对unchecked-go的引用,那么它可能会被插入到一个新的表达式(unchecked-go #f 'a)中速,从而导致灾难。datum->syntax过程同样可以类似地用于构建对未报告标识的引用,即使没有宏展开包括对标识的引用。

为了防止滥用未报告的标识,go宏必须使用syntax-protect明确保护其展开:

(define-syntax (go stx)
  (syntax-case stx ()
   [(_ x)
    (syntax-protect #'(unchecked-go 8 x))]))

syntax-protect函数会使从go结果中提取的任何语法对象成为污染。宏展开器拒绝受污染的标识,因此试图从(go 'a)的展开中提取unchecked-go会产生一个标识,该标识不能用于构造新的表达式(或者至少宏展开器不会将接受)。syntax-rulessyntax-id-ruledefine-syntax-rule表自动保护它们的展开结果。

更准确地说,syntax-protect 装备了一个带染料包(dye pack)的语法对象。当一个语法对象被装备起来时,syntax-e会在其结果中污染任何语法对象。同样,datum->syntax在其第一个参数被装备时会污染其结果。最后,如果引用的语法对象的任何部分被装备,那么相应的部分就会在结果语法常量中受到污染。

当然,宏展开器本身必须能够解除语法对象上的污染,以便进一步展开表达式或其子表达式。当语法对象装备有一个染料包时,染料包具有可用于解除染料包的关联检查器。(syntax-protect stx)函数调用实际上是(syntax-arm stx #f #t)的简写,它使用合适的检查器对stx进行检查。在试图展开或编译每个表达式之前,使用syntax-disarm和它的检查器。

与宏展开器将属性从语法转换器的输入复制到其输出(请参见《(part ("(lib scribblings/reference/reference.scrbl)" "stxprops"))》(语法对象属性))的方式大致相同,展开器会将染料包从转换器的输入拷贝到其输出。基于前面的示例,

"n.rkt"

#lang racket
(require "m.rkt")
(provide go-more)
(define y 'hello)
(define-syntax (go-more stx)
  (syntax-protect #'(go y)))

(go-more)的展开引入了对(go y)中未报告的y的引用,并且展开结果是装备起来的,因此无法从展开中提取y。即使go没有使用syntax-protect作为其结果(也许是因为它根本不需要保护unchecked-go),(go y)上的染色包也会传播到最终展开(unchecked-go 8 y)。宏展开器使用syntax-rearm将染料包从转换器的输入传播到其输出。

16.2.7.1 污染模式

在某些情况下,宏实现者打算允许对宏结果进行有限的破坏,而不会影响结果。例如,给定以下define-like-y宏,

"q.rkt"

#lang racket
(provide define-like-y)
(define y 'hello)
(define-syntax (define-like-y stx)
  (syntax-case stx ()
    [(_ id) (syntax-protect #'(define-values (id) y))]))

有人可能在内部定义中使用宏:

(let ()
  (define-like-y x)
  x)

"q.rkt"模块的实现者很可能打算允许使用define-like-y。然而,要将内部定义转换为letrec绑定,必须解构define-like-y生成的的define表,这通常会污染绑定的x和对y的引用。

相反,允许在内部使用define-like-y,因为syntax-protect专门处理以define-values开头的语法列表。在这种情况下,不是装备整个表达式,而是装备语法列表的每个单独元素,将染料包进一步推入列表的第二个元素,以便将它们附加到定义的标识。因此,展开结果(define-values (x) y)中的define-valuesxy是单独被装备的,并且可以对定义进行解构以转换为letrec

就像syntax-protect一样,展开器通过将染料包推送到列表元素中,重新排列以define-values开头的转换结果。因此,define-like-y可以实现为生成(define id y),它使用define而不是define-values。在这种情况下,整个define表首先装备一个染料包,但是当define表展开为define-values时,染料包会移动到各个部分。

宏展开器处理以define-values开头的语法列表结果的方式与处理以define-syntaxes开头结果的方式相同。以begin开头的语法列表结果被类似地处理,只是语法列表的第二个元素被视为所有其它元素(即,立即元素被装备,而不是其内容)。此外,宏展开器递归地应用这种特殊处理,以防宏生成包含嵌套define-values表的begin表。

染料包的默认应用程序可以通过将'taint-mode属性(见《(part ("(lib scribblings/reference/reference.scrbl)" "stxprops"))》(语法对象属性))附加到宏转换器的结果语法对象来覆盖。如果属性值是'opaque,那么语法对象被装备而且不是它的部件。如果属性值为'transparent,则语法对象的各个部分都是装备的。如果属性值是'transparent-binding,那么语法对象的部件和第二个部件的子部件(如define-valuesdefine-syntaxes)被装备。'transparent'transparent-binding模式会在零件上触发递归属性检查,因此可以任意深入地将防护推送到变换器的结果中。

16.2.7.2 污染和代码检查

用于特权的工具(如调试变换器)必须解除展开器中的染料包。权限通过 代码检查器授予。每个染料包记录一个检查器,并且可以使用足够强大的检查器解除语法对象。

当声明一个模块时,声明会捕获current-code-inspector参数的当前值。当模块中定义的宏转换器应用syntax-protect时,将使用捕获的检查器。一个工具可以通过向syntax-disarm提供与模块检查器相同的检查器或超级检查器。不受信任的代码最终会在将current-code-inspector设置为功能较弱的检查器后运行(在加载了可信代码(如调试工具)之后)。

通过这种安排,宏生成宏需要小心一些,因为生成宏可能会在生成的宏中嵌入语法对象,这些对象需要具有生成模块的保护级别,而不是包含生成宏的模块的保护级别。为了避免这个问题,请使用模块的声明时间检查器,该检查器可以作为(variable-reference->module-declaration-inspector (#%variable-reference))访问,并使用它来定义syntax-protect的变体。

例如,假设go宏是通过一个宏实现的:

#lang racket
(provide def-go)
(define (unchecked-go n x)
  (+ n 17))
(define-syntax (def-go stx)
  (syntax-case stx ()
    [(_ go)
     (protect-syntax
      #'(define-syntax (go stx)
          (syntax-case stx ()
            [(_ x)
             (protect-syntax #'(unchecked-go 8 x))])))]))

def-go在另一个模块中用于定义go时,以及当go定义模块与def-go定义模块处于不同的保护级别时,生成的宏使用protect-syntax是不正确的。使用unchecked-go应该在def-go定义模块的级别上进行保护,而不是在go定义模型的级别上。

解决方案是定义并使用go-syntax-protect,而不是:

#lang racket
(provide def-go)
(define (unchecked-go n x)
  (+ n 17))
(define-for-syntax go-syntax-protect
  (let ([insp (variable-reference->module-declaration-inspector
               (#%variable-reference))])
    (lambda (stx) (syntax-arm stx insp))))
(define-syntax (def-go stx)
  (syntax-case stx ()
    [(_ go)
     (protect-syntax
      #'(define-syntax (go stx)
          (syntax-case stx ()
           [(_ x)
            (go-syntax-protect #'(unchecked-go 8 x))])))]))

16.2.7.3 受保护的导出

有时,一个模块需要将绑定导出到一些模块——与导出模块处于相同信任级别的其他模块——但阻止来自不受信任模块的访问。此类导出应使用provide中的protect-out表。例如,在这个意义上,ffi/unsafe将其所有不安全绑定导出为受保护的(protected)

代码检查器同样提供了确定哪些模块是可信的,哪些模块是不可信的机制。当一个模块被声明时,current-code-inspector的值与模块声明相关联。当一个模块被实例化时(即,当声明的主体被实际执行时),会创建一个子检查器来保护模块的导出。访问模块的受保护导出需要在检查器层次结构中比模块的实例化检查器更高的代码检查器;请注意,模块的声明检查器始终高于其实例化检查器,因此使用相同的代码声明模块,检查器可以访问彼此的导出。

模块中的语法对象常量(如模板中的文字标识)保留其源模块的检查器。通过这种方式,来自受信任模块的宏可以在不受信任的模块中使用,并且宏展开中的受保护标识仍然有效,即使它们最终出现在不受信赖的模块中。当然,这些标识符应该是装备的,以便它们不能从宏展开中提取出来并被不可信代码滥用。

不幸的是,从".zo"文件编译的代码本质上是不可信的,因为它可以通过compile以外的方法进行合成。当编译代码被写入".zo"文件的被编译代码本质上是不可信的。当编译后的代码写入到一个".zo"文件时,编译代码中的语法对象常量会丢失其检查器。编译代码中的所有语法对象常量都会获取加载代码时封装模块的声明时间检查器。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值