11. 语法扩展和模块

本章描述了 Chez Scheme 对 syntax-case 语法抽象机制所做的扩展,这些扩展现已被《Scheme语言修订6报告》所标准化。这些扩展包括模块系统(11.5 节)、元定义(11.8 节)、按情况展开(11.9 节)、syntax-rules 防护板、fluid-let-syntax 以及 include

11.1 fluid 关键字绑定

通过《Scheme语言修订6报告》中规定的 define-syntaxlet-syntax 以及 letrec-syntax 形式所定义的关键字绑定,可以使用 fluid-let-syntax 临时地重新绑定。

  • 语法:(fluid-let-syntax ((keyword expr) ...) form1 form2 ...)
  • 返回:参考解释
  • 所属库:(chezscheme)

每一条 expr 必须求值为一个转换器。fluid-let-syntax 类似于标准的 let-syntax,只不过不会为关键字 keyword ... 引入新绑定,fluid-let-syntax 会在展开它的程序体的时候,临时修改关键字已有的绑定。也就是说,在展开 form1 form2 ... 的时候,每个 keyword 可见的(或顶层的)词法绑定被临时地替换为一个新的关联——即一个关键字与其对应的转换器。对指向相同词法(或顶层)绑定的关键字的所有引用都会受到影响,无论引用是在出现在程序体代码中还是在展开期引入。相反,let-syntax 仅捕获出现在程序体代码中的那些引用。

下面的例子展示了 fluid-let-syntaxlet-syntax 的不同:

(let ([f (lambda (x) (+ x 1))])
  (let-syntax ([g (syntax-rules ()
                    [(_ x) (f x)])])
    (let-syntax ([f (syntax-rules ()
                      [(_ x) x])])
      (g 1)))) ; => 2 

(let ([f (lambda (x) (+ x 1))])
  (let-syntax ([g (syntax-rules ()
                    [(_ x) (f x)])])
    (fluid-let-syntax ([f (syntax-rules ()
                            [(_ x) x])])
      (g 1)))) ; => 1

除了在第二条表达式中, let-syntax 形式内部表达式中的第一条表达式是 fluid-let-syntax 形式以外,这两条表达式都是相同的。在第一条表达式中,出现在展开 (g 1) 过程中的 f,引用的是由 let 式所绑定的变量 f;而在第二条表达式中的 f,引用的是由 fluid-let-syntax 所绑定的关键字 f

下面的例子利用 fluid-let-syntax 来定义 define-integrable 形式,后者类似于用于过程定义的 define,但无论在何处直接调用该过程,都导致该过程的代码被集成或插入到该位置。通过 define-integrabledefine 来定义的过程没有语义差异,只不过在顶层, define-integrable 形式必须在首次引用所定义的标识符之前出现。词法作用域被保持了,集成调用中的实际参数只在适当的时间求值一次,可集成过程可以用作第一类值,并且递归过程不会导致无限递归展开。

(define-syntax define-integrable
  (syntax-rules (lambda)
    [(_ name (lambda formals form1 form2 ...))
     (begin
       (define xname
         (fluid-let-syntax ([name (identifier-syntax xname)])
           (lambda formals form1 form2 ...)))
       (define-syntax name
         (lambda (x)
           (syntax-case x ()
             [_ (identifier? x) #'xname]
             [(_ arg (... ...))
              #'((fluid-let-syntax ([name (identifier-syntax xname)])
                   (lambda formals form1 form2 ...))
                  arg
                  (... ...))]))))]))

define-integrable 的语法如下:

(define-integrable name lambda-expression)

define-integrable 形式会被展开为一组定义构成的序对:关于语法 name 的定义以及关于变量 xname 的定义。name 所对应的转换器会将对 name 的调用展开为一个对 lambda 表达式的直接调用。由于展开的结果不过是直接的 lambda 应用(等价于 let 表达式),因此在求值过程的体之前,正如所要求的那样,实际参数只会刚好被求值一次。所有指向 name 的引用都会被指向 xname 的引用所替代。xname 的定义式将其绑定到 lambda 表达式的值上。这使得该过程可以被用作第一类值。由于 xname 是由转换器引入的,因此除了由 name 对应的转换器引入的对它的引用,xname 的绑定在其它任何地方都是不可见的。

无论 name 出现在 lambda 表达式内部的任何位置,它都会被重新绑定到一个转换器上,用来将所有的引用展开为对 xname 的引用。以这样的目的使用 fluid-let-syntax 可以防止无限展开可集成过程中的间接递归。这样允许过程可以递归调用而不会导致无限展开。 define-integrable 没有特意维护词法作用域,这是因为词法作用域由展开器自动维护。

Chez Scheme 在适当时自动集成局部定义的过程。但是,它无法集成在顶层定义的过程,因为随时可以(通过 evalload )将为顶层变量的赋值代码引入系统。尽管编译器负责局部绑定的过程的集成,但 define-integrable 可用于强制集成在顶层定义的过程。它还可以用于强制编译器集成那些通常不会集成的大型过程。(expand/optimize 过程对于确定是否进行集成非常有用。)

11.2 syntax-rules 转换器

Chez Scheme 扩展了 syntax-rules 以便允许子句中使用类似于 syntax-case 子句中的防护板(fender)。

  • 语法:(syntax-rules (literal ...) clause ...)
  • 返回:转换器
  • 所属库:(chezscheme)

每个 literal 必须是除下划线(_)、省略号(...)以外的标识符。子句必须是下列形式之一:

(pattern template)
(pattern fender template)

《Scheme语言修订6报告》只支持第一种形式。

11.3 syntax-case 转换器

Chez Scheme 提供了一些过程和语法形式,可用于简化某些语法抽象的编写。

  • 过程:(syntax->list syntax-object)
  • 返回:一个由语法对象组成的表
  • 所属库:(chezscheme)

此过程接受一个代表表结构形式的语法对象,并返回由语法对象组成的表,其中的每个语法对象与输入形式的子形式相对应。

syntax->list 可以按如下定义:

(define syntax->list
  (lambda (ls)
    (syntax-case ls ()
      [() '()]
      [(x . r) (cons #'x (syntax->list #'r))]))) 

#'(a b c) ; =>  #<syntax (a b c)>
(syntax->list #'(a b c)) ; => (#<syntax a> #<syntax b> #<syntax c>)

对于从单个模式变量值或模式变量值序列构造的表,不需要调用 syntax->list,因为这样的结构已经是表。例如:

(list? (with-syntax ([x #'a] [y #'b] [z #'c]) #'(x y z)))) ; => #t
(list? (with-syntax ([(x ...) #'(a b c)]) #'(x ...))) ; =>  #t
  • 过程:(syntax->vector syntax-object)
  • 返回:一个由语法对象组成的向量
  • 所属库:(chezscheme)

此过程接受表示向量结构形式的语法对象,并返回由语法对象组成的向量,其中的每个语法对象与输入形式的子形式相对应。

syntax->vector 可以按如下定义:

(define syntax->vector
  (lambda (v)
    (syntax-case v ()
      [#(x ...) (apply vector (syntax->list #'(x ...)))]))) 

#'#(a b c) ; => #<syntax #(a b c)>
(syntax->vector #'#(a b c)) ; => #(#<syntax a> #<syntax b> #<syntax c>)

对于从单个模式变量值或模式变量值序列构造的向量,不需要调用 syntax->vector,因为这样的结构已经是向量。例如:

(vector? (with-syntax ([x #'a] [y #'b] [z #'c]) #'#(x y z)))) ; => #t
(vector? (with-syntax ([(x ...) #'(a b c)]) #'#(x ...))) ; => #t
  • 过程:(syntax-object->datum obj)
  • 返回: 将 obj 的语法信息剔除后的数据
  • 所属库:(chezscheme)

syntax-object->datum 跟《Scheme语言修订6报告》中的 syntax->datum 完全相同。

  • 语法:(datum template)
  • 返回:如下
  • 所属库:(chezscheme)

(datum template) 是一种方便的简写语法,表示:

(syntax->datum (syntax template))

可以像下面这样简单地定义 datum

(define-syntax datum
  (syntax-rules ()
    [(_ t) (syntax->datum #'t)])) 

(with-syntax ((a #'(a b c))) (datum a)) ; => (a b c)
  • 过程:(datum->syntax-object template-identifier obj)
  • 返回:一个语法对象
  • 所属库:(chezscheme)

datum->syntax-object 跟《Scheme语言修订6报告》中的 datum->syntax 完全相同。

  • 语法:(with-implicit (id0 id1 ...) body1 body2 ...)
  • 返回:见如下
  • 所属库:(chezscheme)

这个形式提取了用于创建隐式标识符的 datum->syntax 的常见用法(参见上文)。形式:

(with-implicit (id0 id1 ...)
  body1 body2 ...)

等同于:

(with-syntax ([id1 (datum->syntax #'id0 'id1)] ...)
  body1 body2 ...)

可以像下面这样简单地定义 with-implicit

(define-syntax with-implicit
  (syntax-rules ()
    [(_ (tid id ...) b1 b2 ...)
     (with-syntax ([id (datum->syntax #'tid 'id)] ...)
       b1 b2 ...)]))

我们可以用 with-implicit 来简化上述的 loop 实现(这也是正确的版本)。

(define-syntax loop
  (lambda (x)
    (syntax-case x ()
      [(k e ...)
       (with-implicit (k break)
         #'(call-with-current-continuation
             (lambda (break)
               (let f () e ... (f)))))])))
  • 语法:(include path)
  • 返回:未定义
  • 所属库:(chezscheme)

path 必须是一个字符串。include 形式被展开为一个 begin 表达式,后者包含了在名为 path 的文件中找到的形式。例如,如果文件 f-def.ss 包含了 (define f (lambda () x)),那么表达式:

(let ([x "okay"])
  (include "f-def.ss")
  (f))

会被求值为 "okay"。如果 include 形式出现在定义序列中,并且由 path 指定的文件中的形式都是定义,则 include 形式就被视为定义,如上述例子所示。如果文件包含的是表达式,则 include 形式将被视为表达式。

尽管 Chez Scheme 使用了依赖于实现的定义,以便能够捕获和维护被 include 的代码的源信息,但是可移植版本的 include 可以按如下定义:

(define-syntax include
  (lambda (x)
    (define read-file
      (lambda (fn k)
        (let ([p (open-input-file fn)])
          (let f ([x (read p)])
            (if (eof-object? x)
                (begin (close-input-port p) '())
                (cons (datum->syntax k x)
                      (f (read p))))))))
    (syntax-case x ()
      [(k filename)
       (let ([fn (datum filename)])
         (with-syntax ([(exp ...) (read-file fn #'k)])
           #'(begin exp ...)))])))

include 的定义使用 datum->syntax 将从文件读取的对象转换为适当词法上下文中的语法对象,以便这些表达式中的标识符的定义和引用都在 include 形式出现的范围内。

在 Chez Scheme 的 include 实现中,参数对象 source-directories(12.5节)是一个目录集合,用于搜索未由绝对路径名标识的源文件。

  • 过程:(syntax-error obj string ...)
  • 返回:不返回
  • 所属库:(chezscheme)

可以使用 syntax-error 报告语法错误,该语法错误通过串联 string ...obj 的打印表示来生成消息。如果未提供字符串参数,则使用字符串 "invalid syntax"。 如果 obj 是语法对象,则在创建打印表示之前,会剥离语法对象的包装器(与syntax->datum 一样)。如果源文件信息存在于 syntax-object 包装器中,则 syntax-error 将此信息合并到错误消息中。

如果输入无法匹配其中一个子句,则 syntax-casesyntax-rules 会自动调用 syntax-error

在以下(匿名)let 形式的定义中,我们可以使用 syntax-error 来精确地报告检测到的错误的诱因。

(define-syntax let
  (lambda (x)
    (define check-ids!
      (lambda (ls)
        (unless (null? ls)
          (unless (identifier? (car ls))
            (syntax-error (car ls) "let cannot bind non-identifier"))
          (check-ids! (cdr ls)))))
    (define check-unique!
      (lambda (ls)
        (unless (null? ls)
          (let ([x (car ls)])
            (when (let mem? ([ls (cdr ls)])
                    (and (not (null? ls))
                         (or (bound-identifier=? x (car ls))
                             (mem? (cdr ls)))))
              (syntax-error x "let cannot bind two occurrences of")))
          (check-unique! (cdr ls)))))
    (syntax-case x ()
      [(_ ((i e) ...) b1 b2 ...)
       (begin
         (check-ids! #'(i ...))
         (check-unique! #'(i ...))
         #'((lambda (i ...) b1 b2 ...) e ...))])))

有了这些变动,下面的表达式:

(let ([a 3] [a 4]) (+ a a))

会产生这样的错误消息:"let cannot bind two occurrences of a."

  • 过程:(literal-identifier=? identifier1 identifier2)
  • 返回:见下
  • 所属库:(chezscheme)

这个过程同《Scheme语言修订6报告》中的 free-identifier=? 一致,提供此函数只是为了保证向后兼容。

11.4 编译期值与编译期属性

定义一系列互相依赖的宏时,在展开器所使用的、用来记录有关变量、关键字、模块名等信息的同一个编译环境中,将信息附加在标识符上通常会带来方便。例如,一个用于定义记录类型的宏,如 define-record-type,可能需要在编译期环境中将信息附加到该记录类型的名称中,以用于处理子记录类型的定义。

Chez Scheme 提供了两种机制,用于在编译期环境中将信息附加到标识符上:编译期值和编译期属性。编译期值是一种转换器,可以通过 define-syntaxlet-syntaxletrec-syntaxfluid-let-syntax 与标识符相关联。当标识符与编译期值相关联时,它不可以再具有其他的含义,并且尝试将其作为普通标识符引用会导致语法错误。另一方面,编译期属性与现有绑定一起维护,提供有关绑定的其他信息。当对标识符进行普通引用时,将忽略属性。

宏用于获取编译期值和属性的机制是类似的。在这两种情况下,宏的转换器返回的不是一个语法对象,而是一个过程 p 。展开器使用一个环境查找过程 lookup 作为参数调用 p ,然后 p 可以在构造宏的最终输出之前使用它来获取一个或多个标识符的编译期值和属性。 lookup 接受一个或两个标识符作为参数。当使用一个参数 id 时,lookup 返回 id 的编译期值,如果 id 没有编译期值,则返回#f。 使用 idkey 两个参数时,lookup 返回 idkey 属性的值,如果 id 没有 key 属性,则返回 #f

  • 过程: (make-compile-time-value obj)
  • 返回:一个编译期值
  • 所属库:(chezscheme)

编译期值是一种转换器,使得一个关键字可以通过任何关键字绑定结构与之关联,例如 define-syntaxlet-syntax 。转换器封装了所提供的 obj 。被封装的对象可以按照上述方式进行检索。

以下示例演示了如何使用此功能来定义简单的、基于语法的记录类型定义机制,其中记录类型描述符是在展开期生成的。

(define-syntax drt
  (lambda (x)
    (define construct-name
      (lambda (template-identifier . args)
        (datum->syntax template-identifier
          (string->symbol
            (apply string-append
              (map (lambda (x)
                     (if (string? x)
                         x
                         (symbol->string (syntax->datum x))))
                   args))))))
    (define do-drt
      (lambda (rname fname* prtd)
        (with-syntax ([rname rname]
                      [rtd (make-record-type-descriptor
                             (syntax->datum rname) prtd #f #f #f
                             (list->vector
                               (map (lambda (fname)
                                      `(immutable ,(syntax->datum fname)))
                                    fname*)))]
                      [make-rname (construct-name rname "make-" rname)]
                      [rname? (construct-name rname rname "?")]
                      [(rname-fname ...)
                       (map (lambda (fname)
                              (construct-name fname rname "-" fname))
                            fname*)]
                      [(i ...) (enumerate fname*)])
          #'(begin
              (define-syntax rname (make-compile-time-value 'rtd))
              (define rcd (make-record-constructor-descriptor 'rtd #f #f))
              (define make-rname (record-constructor rcd))
              (define rname? (record-predicate 'rtd))
              (define rname-fname (record-accessor 'rtd i))
              ...))))
    (syntax-case x (parent)
      [(_ rname (fname ...))
       (for-all identifier? #'(rname fname ...))
       (do-drt #'rname #'(fname ...) #f)]
      [(_ rname pname (fname ...))
       (for-all identifier? #'(rname pname fname ...))
       (lambda (lookup)
         (let ([prtd (lookup #'pname)])
           (unless (record-type-descriptor? prtd)
             (syntax-error #'pname "unrecognized parent record type"))
           (do-drt #'rname #'(fname ...) prtd)))])))

(drt prec (x y))
(drt crec prec (z))
(define r (make-crec 1 2 3))
(prec? r) ; => #t
(prec-x r) ; => 1
(crec-z r) ; => 3
prec ; => exception: invalid syntax prec
  • 语法:(define-property id key expr)
  • 返回:未定义
  • 所属库:(chezscheme)

define-property 形式将属性附加到现有标识符绑定,而不会干扰该绑定范围内标识符的现有含义。它通常由一个宏用于记录有关绑定的信息,以供另一个宏使用。idkey 都必须是标识符。表达式 expr 会在 define-property 形式被展开时被计算,新属性是由 keyexpr 的值所形成的关联,并将其附加到 id 的现有绑定,该绑定必须具有可见的局部或顶层绑定。

define-property 属于定义,可以出现在其他定义可以出现的任何地方。define-property 引入的属性的范围是 define-property 形式所出现的整个程序体,如果它出现在顶层,则是全局的,除非它由相同 idkey 的属性替换,或者它所附着的标识符的绑定被遮蔽了。可以使用不同的键将任意数量的属性附加到同一个绑定。将与已附加的属性同名的新属性附加到绑定上,会使新属性遮蔽现有属性。

下面的示例定义了一个宏 get-info,它检索绑定的 info 属性,并定义变量 x,将info 属性附加到 x 的绑定,通过 get-info 检索属性,引用 x 以显示其正常绑定仍然完好无损,并在 x 的不同绑定的作用域内再次使用 get-info ,以显示绑定与编译期值同时被外部的 x 遮蔽。

(define info)
(define-syntax get-info
  (lambda (x)
    (lambda (lookup)
      (syntax-case x ()
        [(_ q)
         (let ([info-value (lookup #'q #'info)])
           #`'#,(datum->syntax #'* info-value))]))))
(define x "x-value")
(define-property x info "x-info")
(get-info x) ; => "x-info"
x ; => "x-value"
(let ([x "inner-x-value"]) (get-info x)) ; => #f

调试时,在给定标识符和键的情况下,拥有一个检索任意属性的形式通常很有用。下面的 get-property 宏就是这样。

(define-syntax get-property
  (lambda (x)
    (lambda (r)
      (syntax-case x ()
        [(_ id key)
         #`'#,(datum->syntax #'* (r #'id #'key))]))))
(get-property x info) ; => "x-info"

在使用 get-property 的情况下,两个标识符的绑定都必须可见。

下面定义的 drt 版本类似于使用上面的 make-compile-time-value 定义的版本,只不过它将记录名称定义为一个宏,并且使用更具描述性的消息引发异常,同时将记录类型描述符作为一个单独的属性附加到绑定上。与 drt 一起定义的变量 drt-key 仅用作 drt 附加到记录名称的属性的键。drt-keydrt 都在一个只导出后者的模块中定义,确保 drt 使用的属性不能被访问或伪造。

(library (drt) (export drt) (import (chezscheme))
  (define drt-key)
  (define-syntax drt
    (lambda (x)
      (define construct-name
        (lambda (template-identifier . args)
          (datum->syntax template-identifier
            (string->symbol
              (apply string-append
                (map (lambda (x)
                       (if (string? x)
                           x
                           (symbol->string (syntax->datum x))))
                     args))))))
      (define do-drt
        (lambda (rname fname* prtd)
          (with-syntax ([rname rname]
                        [rtd (make-record-type-descriptor
                               (syntax->datum rname) prtd #f #f #f
                               (list->vector
                                 (map (lambda (fname)
                                        `(immutable ,(syntax->datum fname)))
                                      fname*)))]
                        [make-rname (construct-name rname "make-" rname)]
                        [rname? (construct-name rname rname "?")]
                        [(rname-fname ...)
                         (map (lambda (fname)
                                (construct-name fname rname "-" fname))
                              fname*)]
                        [(i ...) (enumerate fname*)])
            #'(begin
                (define-syntax rname
                  (lambda (x)
                    (syntax-error x "invalid use of record name")))
                (define rcd (make-record-constructor-descriptor 'rtd #f #f))
                (define-property rname drt-key 'rtd)
                (define make-rname (record-constructor rcd))
                (define rname? (record-predicate 'rtd))
                (define rname-fname (record-accessor 'rtd i))
                ...))))
      (syntax-case x (parent)
        [(_ rname (fname ...))
         (for-all identifier? #'(rname fname ...))
         (do-drt #'rname #'(fname ...) #f)]
        [(_ rname pname (fname ...))
         (for-all identifier? #'(rname pname fname ...))
         (lambda (lookup)
           (let ([prtd (lookup #'pname #'drt-key)])
             (unless prtd
               (syntax-error #'pname "unrecognized parent record type"))
             (do-drt #'rname #'(fname ...) prtd)))]))))

(import (drt))
(drt prec (x y))
(drt crec prec (z))
(define r (make-crec 1 2 3))
(prec? r) ; => #t
(prec-x r) ; => 1
(crec-z r) ; => 3
prec ; => exception: invalid use of record name prec

11.5 模块

模块用于帮助将程序组织成单独的部分,这些部分通过声明好的接口干净地交互。尽管模块化编程通常可以为多人参加的大型程序开发带来便利,但它也可以在 Chez Scheme 中以“微模块”级别使用,因为 Chez Scheme 中的模块和 import 形式属于定义,并且可以出现在定义可以出现的任意位置,包括在 lambda 表达式的程序体或其他局部作用域中。

模块控制绑定的可见性,可以用作扩展词法作用域,以允许更精确地控制绑定的可见范围。模块导出标识符的绑定,即变量绑定、关键字绑定或模块名绑定。模块可以是具名的抑或匿名的。只要模块名可见,那么在具名模块出现处导入的绑定都可见。匿名模块中导出的绑定,在模块出现的地方被隐式地导入。匿名模块可用于隐藏一组绑定中的一些绑定,同时允许其余绑定可见。

本节中给出的一些文本和示例改编自“扩展语法抽象的作用域”[32],后者更详细地描述了模块及其实现。

  • 语法:(module name interface defn ... init ...)
  • 语法:(module interface defn ... init ...)
  • 返回:未定义
  • 所属库:(chezscheme)

name 是标识符,defn ... 是定义,init ... 是表达式。interface 是形如 (export ...) 的导出表,其中每个 export 都是标识符 identifier 抑或形式 (identifier export ...)

module 的第一种语法建立一个具名作用域,用来封装一组标识符绑定。在模块名可见的任何地方,导出的绑定都可以通过 importimport-only(第10.4节)来使其可见。module 的第二种语法引入了一个匿名模块,其 module 形式出现时隐式导入绑定(就好像用一个隐含的模块名调用 import 一样)。

模块由一组(可能为空的)定义和一组(可能为空的)初始化表达式序列组成。模块中定义的标识符在模块的程序体中可见,被导出的标识符在模块的导入作用域内亦可见。模块接口中列出的每个标识符必须在该模块中定义或被导入该到模块。module 形式是一种定义,因此可以出现在其他定义可以出现的任何位置,包括嵌套在 lambda 表达式的程序体、 library 形式、顶层程序,以及嵌套在其他模块中。此外,因为模块名的作用域与其他标识符相同,所以模块和库可以像变量和关键字那样导出模块名。

当接口包含形如 (identifier export ...) 形式的导出时,只有 identifier 在导入的上下文中可见。 export ... 中的标识符是间接导入,就像通过 indirect-export 形式声明一样(第10.4节)。

模块名与其他标识符占用相同的名字空间,并遵循相同的作用域规则。除非被导出,否则模块中定义的标识符仅在该模块中可见。

模块内的表达式可以引用在模块外部绑定的标识符。

(let ([x 3])
  (module m (plusx)
    (define plusx (lambda (y) (+ x y))))
  (import m)
  (let ([x 4])
    (plusx 5))) ; => 8

同样,除了那些被导入的标识符所遮蔽的变量,import 不会阻止存取在该形式出现时可见的标识符。

(module m (y) (define y 'm-y))
(let ([x 'local-x] [y 'local-y])
  (import m)
  (list x y)) ; => (local-x m-y)

另一方面,在模块中使用 import-only 会建立一个隔离的作用域,其中唯一可见的是被导入模块所导出的标识符。

(module m (y) (define y 'm-y))
(let ([x 'local-x] [y 'local-y])
  (import-only m)
  x) ; => Error: x is not visible

静态验证有时需要这个功能来确保除了显式地被导入到模块或局部作用域的标识符,没有使用其他的标识符。

除非一个模块是由 import-only 导入,并且导出了 import 或者 import-only 以及至少一个模块名,否则在 import-only 形式作用域内无法进行后续的导入。为了创建包含多个模块的导出的隔离作用域,而不使 importimport-only 可见 ,所有想要导入的模块必须在同一个 import-only 形式中列出。

另一种解决方案是创建一个包含每个其他模块的导出的模块。

(module m2 (y) (define y 'y))
(module m1 (x) (define x 'x))
(module mega-module (cons x y)
  (import m1)
  (import m2)
  (import scheme))
(let ([y 3])
  (import-only mega-module)
  (cons x y)) ; => (x . y)

在它被编译之前,源程序被翻译成不含语法抽象、语法定义、库定义、模块定义以及 import 形式的核心语言程序。翻译由语法展开器负责,语法展开器以递归下降的方式处理源程序中的形式。

define-syntax 形式在翻译期环境中将关键字与转换器相关联。当展开器遇到关键字时,它会调用关联的转换器并重新处理生成的形式。module 形式将模块名与接口相关联。当展开器遇到 import 形式时,它会从翻译期环境中提取相应的模块接口,并使导出的绑定在 import 形式出现的作用域中可见。

内部定义和 module 形式的程序体中的定义由左至右地进行处理,以便模块的定义和导入可以出现在相同的定义序列中。然而,出现在程序体或变量定义右侧的表达式,只有在处理完整个定义集合后才进行翻译,从而允许变量和语法定义之间的完全互递归。

模块和 import 特殊形式仅影响标识符在源程序中的可见性,而不影响其含义。特别地,无论变量被绑定到在模块内部或外部定义的位置,import 都不会引入新位置。为了保持由模块和语法抽象建立的作用域关系,局部变量在必要时会被重命名。因此,表达式:

(let ([x 1])
  (module m (x setter)
    (define-syntax x (identifier-syntax z))
    (define setter (lambda (x) (set! z x)))
    (define z 5))
  (let ([y x] [z 0])
    (import m)
    (setter 3)
    (+ x y z))) ; => 4

等价于下面的表达式,其中标识符被统一地更名,并带上了下标:

(let ([x0 1])
  (define-syntax x1 (identifier-syntax z1))
  (define setter1 (lambda (x2) (set! z1 x2)))
  (define z1 5)
  (let ([y3 x0] [z3 0])
    (setter1 3)
    (+ x1 y3 z3)))

存在于顶层的 beginlambda、顶层程序,librarymodule 的程序体中的定义,在展开期由展开器从左到右处理,变量定义在运行时从左到右进行计算。 在求值完变量定义之后,依次求值 module 程序体内出现的初始化表达式。

可以通过多种方式定义互递归的模块。在以下程序中,ab 是由匿名模块导出的互递归模块,该匿名模块的局部作用域用于静态地链接两者。例如,模块 a 中的自由变量 y 指的是通过导入闭合模块中的 b 所提供的 y 的绑定。

(module (a b)
  (module a (x) (define x (lambda () y)))
  (module b (y) (define y (lambda () x)))
  (import a)
  (import b))

以下语法抽象泛化了此模式,以允许定义多个互递归的模块。

(define-syntax rec-modules
  (syntax-rules (module)
    [(_ (module m (id ...) form ...) ...)
     (module (m ...)
       (module m (id ...) form ...) ...
       (import m) ...)]))

由于模块可以重新导出所导入的绑定,所以很容易在单个模块上提供多个视图,就像下面的 str 提供的视图那样,或者将几个模块组合成一个复合,就像 r 那样。

(module p (x y)
  (define x 1) (define y 2))
(module q (y z)
  (define y 3) (define z 4))
(module r (a b c d)
  (import* p (a x) (b y))
  (import* q (c y) (d z)))
(module s (a c) (import r))
(module t (b d) (import r))

为了能够让接口与实现分离,以下语法抽象支持具名接口的定义和使用。

(define-syntax define-interface
  (syntax-rules ()
    [(_ name (export ...))
     (define-syntax name
       (lambda (x)
         (syntax-case x ()
           [(_ n defs)
            (with-implicit (n export ...)
              #'(module n (export ...) .
                  defs))])))])) 

(define-syntax define-module
  (syntax-rules ()
    [(_ name interface defn ...)
     (interface name (defn ...))]))

define-interface 创建一个接口宏,给定模块名和定义表,该接口宏将其展开为具有具体接口的模块定义。

with-implicit 用于确保引入的 export 标识符在与 define-module 形式中模块名相同的作用域中可见。

define-interfacedefine-module 可以像下面这样使用:

(define-interface simple (a b))
(define-module m simple
  (define-syntax a (identifier-syntax 1))
  (define b (lambda () c))
  (define c 2))
(let () (import m) (+ a (b))) ; => 3

下面定义的抽象模块的功能允许在求值模块形式时逐步地满足模块接口。使得接口和实现之间可以灵活地分离、支持互递归模块的单独编译,并允许重新定义模块实现。

(define-syntax abstract-module
  (syntax-rules ()
    [(_ name (ex ...) (kwd ...) defn ...)
     (module name (ex ... kwd ...)
       (declare ex) ...
       defn ...)])) 

(define-syntax implement
  (syntax-rules ()
    [(_ name form ...)
     (module () (import name) form ...)]))

abstract-module 形式中,表 ex ... 中的每个导出必须是变量。这些变量的值由一个或多个单独的 implement 形式提供。由于关键字绑定必须在编译期出现,因此不能逐步满足它们,而是将它们列为单独的导出并在抽象模块中定义。

implement 特殊形式内部,形式序列 form ... 是由零个或多个定义组成的序列,后跟零个或多个表达式的序列。由于用于展开 implement 的模块不会导出任何内容,因此对 implement 来说,这些定义都是局部的。表达式可以是任意表达式,但对于那些由 implement 形式提供的变量定义,应该为每个变量包含一个 satisfy 形式。 satisfy 形式的语法为:

(satisfy variable expr)

declare 以及 satisfy 可以简单地等价于 define 以及 set!

(define-syntax declare (identifier-syntax define))
(define-syntax satisfy (identifier-syntax set!))

或者,declare 可以将声明的变量初始化一个只被 decalresatisfy 所知道的标志值,并且 satisfy 可以验证该标志是否仍然存在,以确保满足给定标识符的值的尝试只发起过一次。

(module ((declare cookie) (satisfy cookie))
  (define cookie "chocolate chip")
  (define-syntax declare
    (syntax-rules () [(_ var) (define var cookie)]))
  (define-syntax satisfy
    (syntax-rules ()
      [(_ var exp)
       (if (eq? var cookie)
           (set! var exp)
           (assertion-violationf 'satisfy
             "value of variable ~s has already been satisfied"
             'var))])))

利用 abstract-module 以及 implement,我们可以像下面这样定义互递归、可独立编译的模块:

(abstract-module e (even?) (pred)
  (define-syntax pred
    (syntax-rules () [(_ exp) (- exp 1)]))) 

(abstract-module o (odd?) ()) 

(implement e
  (import o)
  (satisfy even?
    (lambda (x)
      (or (zero? x) (odd? (pred x)))))) 

(implement o
  (import e)
  (satisfy odd?
    (lambda (x) (not (even? x))))) 

(let () (import-only e) (even? 38)) ; => #t
  • 语法:only
  • 语法:except
  • 语法:add-prefix
  • 语法:drop-prefix
  • 语法:rename
  • 语法:alias
  • 所属库:(chezscheme)

这些标识符都是 importimport-only 的辅助关键字。除非是在它们被识别为辅助关键字的上下文中,否则引用这些标识符是违反语法的。

11.6 独立导入以及独立导出形式

第10.4节中描述的局部导入依然适用于模块,该节描述的局部导出形式统一可以在模块内部使用。

11.7 内置模块

Chez Scheme 中有5个内置模块:schemer5rsr5rs-syntaxieee 以及 $system。 每个模块都是不可变的,即,不可以修改由它们导出的绑定。

  • 模块:scheme
  • 所属库:(chezscheme)

scheme 包含了 Chez Scheme 中内置的所有用户可见的顶层绑定(变量、关键字以及模块名)。

  • 模块:r5rs
  • 所属库:(chezscheme)

r5rs 包含了《Scheme语言修订5报告》中定义的所有顶层绑定(变量及关键字)。从 r5rs 导出的绑定,就是以 scheme-report-environment 所返回的环境说明符、调用 eval 求值的表达式时可用的绑定。

  • 模块:r5rs-syntax
  • 所属库:(chezscheme)

r5rs-syntax 包含了《Scheme语言修订5报告》中定义的所有顶层关键字绑定。从r5rs-syntax 导出的绑定,就是以 null-environment 所返回的环境说明符,调用 eval、求值的表达式时可用的绑定。

  • 模块:ieee
  • 所属库:(chezscheme)

ieee 包含了 ANSI/IEEE Scheme 标准中定义的所有顶层绑定(变量及关键字)。从 ieee 导出的绑定,就是以 ieee-environment 所返回的环境说明符、调用 eval 求值的表达式时可用的绑定。

  • 模块:$system
  • 所属库:(chezscheme)

$system 包含了内置于 Chez Scheme 中所有用户可见的顶层绑定,它们是各种各样的未被文档化的系统绑定。

11.8. 元定义

  • 语法:(meta . definition)
  • 返回:未定义
  • 所属库:(chezscheme)

meta 关键字实际上是一个可以放在任何定义关键字前面的前缀,例如,

(meta define x 3)

它告诉展开器,(该条)定义式产生的任何变量定义都只是展开期定义,只能用于其他元定义的右侧,最主要是用在转换器表达式中。它用于定义展开期辅助函数,以及供一个或多个 syntax-case 转换器所使用的其他信息。

(module M (helper1 a b)
  (meta define helper1
    (lambda (---)
      ---))
  (meta define helper2
    (lambda (---)
      --- (helper2 ---) ---))
  (define-syntax a
    (lambda (x)
      --- (helper1 ---) ---))
  (define-syntax b
    (lambda (x)
      --- (helper1 ---) ---
      --- (helper2 ---) ---)))

语法定义或元定义的右侧表达式只能引用其值在编译期环境中已可用的标识符。由于库、模块、lambda 之类的程序体是从左至右展开的,这意味着元定义序列的语义类似于 let*,其中每个右侧只能引用在序列的早期定义的变量。一个特例是,只要对自己名字的求值,发生在定义表达式的计算之后,元定义的右侧可以引用它自己的名称。这使得元定义可以是自递归的,但不是互递归的。但是,元定义的右侧可以构建语法对象,其中包含在元定义出现的程序体中定义的任何标识符。

元定义通过宏展开传播,因此可以编写,例如:

(module (a)
  (meta define-record foo (x))
  (define-syntax a
    (let ([q (make-foo #''q)])
      (lambda (x) (foo-x q)))))
a ;=> q

其中 define-record 是一个展开为一组定义的宏。

有时可以用:

(meta begin defn ...)

或者

(meta module {exports} defn ...)

或者

(meta include "path")

来方便地创建一组元绑定。

11.9 按条件展开

可以通过 meta-cond 在展开期做出决策,这类似于 cond,但是是在展开期求值并测试表达式,并且可以在预期为定义的上下文中以及在表达式上下文中使用。

  • 语法:(meta-cond clause1 clause2 ...)
  • 返回:见下
  • 所属库:(chezscheme)

除了最后一个 clause,其余的必须采取以下形式:

(test expr1 expr2 ...)

最后一个 cluase 除可采取上述形式外,还可以使用下面的 else 子句形式:

(else expr1 expr2 ...)

在展开期间,test 表达式会被顺序地求值,直到某条表达式求值为真或者求值完所有的表达式为止。如果一条 test 表达式求值为真,则 meta-cond 形式将展开为包含相应表达式 expr1 expr2 ... 所组成的 begin 形式。如果没有求值为真的 test 表达式,且存在 else 子句,则 meta-cond 形式将展开为由来自于 else 子句的表达式 expr1 expr2 ... 所组成的 begin 形式。否则,meta-cond 表达式展开为对 void 过程的调用。

meta-cond 可以按如下定义:

(define-syntax meta-cond
  (syntax-rules ()
    [(_ [a0 a1 a2 ...] [b0 b1 b2 ...] ...)
     (let-syntax ([expr (cond
                          [a0 (identifier-syntax (begin a1 a2 ...))]
                          [b0 (identifier-syntax (begin b1 b2 ...))]
                          ...)])
       expr)]))

meta-cond 用于在展开期从一组可能的形式中进行选择。例如,程序员可以定义过程的安全(进行错误检查)和不安全(不进行错误检查)版本,并根据编译期优化级别决定调用版本,如下所示。

(meta-cond
  [(= (optimize-level) 3) (unsafe-frob x)]
  [else (safe-frob x)])

11.10 别名

  • 语法:(alias id1 id2)
  • 返回:未定义
  • 所属库:(chezscheme)

alias 是一类定义,可以出现在其他定义可以出现的任何位置。它用于将绑定从一个标识符转移到另一个标识符。

(let ([x 3]) (alias y x) (set! y 4) (list x y)) ; => (4 4) 

(module lisp (if)
  (module (scheme:if)
    (import scheme)
    (alias scheme:if if))
  (define-syntax if
    (syntax-rules ()
      [(_ e_1 e_2 e_3)
       (scheme:if (not (memq e_1 '(#f ()))) e_2 e_3)])))
(define (length ls)
  (import lisp)
  (if ls (+ (length (cdr ls)) 1) 0))
(length '(a b c)) ; => 3

由于展开是由左至右进行的,别名应该出现在右侧的标识符的定义之后,例如:

(let ()
  (import-only (chezscheme))
  (define y 3)
  (alias x y)
  x) ; => 3

而不是:

(let ()
  (import-only (chezscheme))
  (alias x y)
  (define y 3)
  x) ; => exception: unbound identifier

11.11 注解

当通过 loadcompile-file 或这类过程的变体(例如 load-library)从文件中读取源代码时,读取器会将注解附加到从文件读取的每个对象。这些注解将文件中的对象与它们所在的文件及位置联系起来。编译过程会追踪注解,并在运行时将其与编译后的代码关联起来。展开器和编译器使用注解来生成语法错误和编译器警告,以识别非法形式的位置,并且审查器使用它们来标识调用的位置和过程定义。编译器和运行时也使用注解将源位置与性能分析计数相关联。

虽然这些注解通常是“幕后”维护的,但程序员可以通过一组用于创建和访问注解的过程直接操作它们。

注解是一种不同与其他类型,并且具有四个组件的值:表达式(可能还有带注解的子表达式)、源对象、表达式的剥离版本和用法选项。可以通过 make-annotation 创建注解,该注解具有对应于前三个组件的三个必选参数和对应于第四个组件的可选参数。第二个参数必须是一个源对象,第三个参数应该是第一个参数的剥离版本,即等同于第一个参数的每个注解被其表达式组件替换的结果。用作表示源代码时,注解本质上等同于其剥离的组件,只不过注解附带有源信息并可供展开器或求值器使用。可选的第四个参数(如果存在)必须是在符号 debugprofile 上设置的枚举,并且默认为包含 debugprofile 的枚举集。

标记为 debug 的注解用于编译期错误报告和运行时错误报告及审查; 标记为 profile 的注解用于性能分析。 Scheme 读取器创建的注解始终同时标记为 debugprofile,但其他读取器和解析器可能选择将某些注解仅仅标记为 debug 抑或为 profile。特别是为了调试,在具有相同源对象的解析器输出中,为多条表达式做注解可能很有用,并且,仅将其中一个标记为 profile 以避免重复计数。不将任何表达式标记为 profile 转而使用显式的 profile 形式(12.7节)来指明要做性能分析的源位置集也很有用。

源对象也是一种不同与其他类型,并且还具有三个或五个组件的值:源文件描述符(sfd)、起始文件位置(bfp)、截止文件位置(efp)、可选的起始行和一个可选的起始列。sfd 标识了用于从中读取表达式的文件,bfp 和 efp 标识文件中对象占据的字符位置范围,bfp 所指向的字符属于该对象,而 efp 对应的字符不属于源对象。行和列都是数字抑或两者都不存在。可以通过 make-source-object 创建源对象,该对象采用与这些组件对应的三个或五个参数。第一个参数必须是源文件描述符,第二个和第三个必须是非负精确整数,第二个必须不大于第三个,如果提供了第四个和第五个参数,那么必须是精确的正整数。

源文件描述符也是一种不同与其他类型,并且具有两个组件的值:文件的路径(由字符串表示)和校验和(由数字表示)。取决于创建源文件描述符时如何指定文件路径,路径可能是绝对的,也可能不是绝对的。校验和是根据文件的长度和内容在创建文件时计算的,并由查找源文件的工具进行检查,以确保找到正确的文件并且尚未修改。源文件描述符可以使用 make-source-file-descriptor 创建,它接受两个参数:代表路径的字符串和二进制输入端口,以及可选的第三个默认为 false 的布尔参数 reset?make-source-file-descriptor 从当前位置开始,根据端口的内容计算校验和。如果 reset? 为真,在计算校验和之后,它会使用 set-port-position! 重置端口;否则,它将端口留在文件末尾。

用于创建、检查和访问注解、源对象和源文件描述符的过程总结如下,并在本节后面更详细地介绍。

(make-annotation obj source-object obj) ; => annotation
(annotation? obj) ; => boolean
(annotation-expression annotation) ; => obj
(annotation-source annotation) ; => source-object
(annotation-stripped annotation) ; => obj 

(make-source-object sfd uint uint) ; => source-object
(make-source-object sfd uint uint uint uint) ; => source-object
(source-object? obj) ; => boolean
(source-object-sfd source-object) ; => sfd
(source-object-bfp source-object) ; => uint
(source-object-efp source-object) ; => uint
(source-object-line source-object) ; => uint or #f
(source-object-column source-object) ; => uint or #f 

(make-source-file-descriptor string binary-input-port) ; => sfd
(make-source-file-descriptor string binary-input-port reset?) ; => sfd
(source-file-descriptor? obj) ; => boolean
(source-file-descriptor-checksum sfd) ; => obj
(source-file-descriptor-path sfd) ; => obj

程序可以使用 open-file-input-port 打开源文件,使用 make-source-file-descriptor 创建 sfd ,使用 transcoded-port 从二进制端口创建文本端口,并为它从文件中读取的每个对象创建源对象和注解。如果不需要自定义读取器,则可以使用 Scheme 读取器通过 get-datum/annotations 过程读取注解:

(get-datum/annotations textual-input-port sfd uint) ; => obj, uint

get-datum/annotationsget-datum 类似,但它不返回普通数据,而是返回一个注解,后者封装有数据(可能带有嵌套的注解)、源对象和(剥离了注解的)普通数据。它还返回第二个值,即文件中对象之后的第一个字符的位置。get-datum/annotations 接受并返回字符位置,以便文本端口不需要支持 port-position ,如果它支持 port-position,则不需要以字符报告位置。(位置通常以字节为单位报告)get-datum/annotations 返回的注解中记录的 bfp 和 efp 位置的正确性依赖于所提供的位置的正确性。

读取后,可以将注解传递给展开器、解释器或编译器。evalexpandinterpretcompile 过程都接受带注解或不带注解的输入。

另外两个过程补全了与注解相关的原语集:

(open-source-file sfd) ; => #f or port
(syntax->annotation obj) ; => #f or annotation

open-source-file 尝试查找并打开由 sfd 标识的源文件。如果成功,它返回一个文本输入端口,并将游标置于文件的开头,否则返回 #f

syntax->annotation 接受语法对象。如果语法对象的表达式带有注解,则返回注解;否则它返回 #f。宏可以使用它从输入形式中提取源信息(如果可用)。

过程 datum->syntax 接受带注解或不带注解的输入数据。

  • 过程:(make-annotation obj source-object stripped-obj)
  • 过程:(make-annotation obj source-object stripped-obj options)
  • 返回:一个注解
  • 所属库:(chezscheme)

注解是由 obj 作为其表达式组件,source-object 作为其源对象组件,stripped-obj 作为其剥离组件所形成。obj 应该表示一个表达式,可能带有嵌套的注解。 stripped-obj 应该是 obj 的剥离版本,即等效于 obj 的每个注解被其表达式组件替换。options(如果存在)必须是在符号 debugprofile 上设置的枚举,并且默认为包含 debugprofile 的枚举集。标记为 debug 的注解用于编译期错误报告、运行时错误报告和检查;标记为 profile 的注解用于做性能分析。

  • 过程:(annotation? obj)
  • 返回:如果 obj 是一个注解的话,则返回 #t,否则返回 #f
  • 所属库:(chezscheme)

  • 过程:(annotation-expression annotation)

  • 返回:annotationexpression 组件
  • 所属库:(chezscheme)

  • 过程:(annotation-source annotation)

  • 返回:annotationsource-object 组件
  • 所属库:(chezscheme)

  • 过程:(annotation-stripped annotation)

  • 返回:annotationstripped 组件
  • 所属库:(chezscheme)

  • 过程:(annotation-options annotation)

  • 返回:annotationoptions 枚举集
  • 所属库:(chezscheme)

  • 过程:(make-source-object sfd bfp efp)

  • 过程:(make-source-object sfd bfp efp line column)
  • 返回:一个 source-object
  • 所属库:(chezscheme)

sfd 必须是源文件描述符。bfpefp 必须是精确的非负整数,bfp 不应大于efplinecolumn 必须是精确的正整数。

  • 过程:(source-object? obj)
  • 返回:如果 obj 是一个源对象则返回 #t,否则返回 #f
  • 所属库:(chezscheme)

  • 过程:(source-object-sfd source-object)

  • 返回:source-object 的 sfd 组件
  • 所属库:(chezscheme)

  • 过程:(source-object-bfp source-object)

  • 返回:source-object 的 bfp 组件
  • 所属库:(chezscheme)

  • 过程:(source-object-efp source-object)

  • 返回:source-object 的 efp 组件
  • 所属库:(chezscheme)

  • 过程:(source-object-line source-object)

  • 返回:如果 source-object 的 line 组件存在,则返回该组件,否则返回 #f
  • 所属库:(chezscheme)

  • 过程:(source-object-column source-object)

  • 返回:如果 source-object 的 column 组件存在,则返回该组件,否则返回 #f
  • 所属库:(chezscheme)

  • 线程级参数对象:current-make-source-object

  • 所属库:(chezscheme)

读取器使用 current-make-source-object 为注解构造源对象。current-make-source-object 最初绑定到 make-source-object,并且读取器总是使用三个参数调用绑定到参数对象的函数。

例如,将此参数对象调整为立即地将位置整数转换为文件位置对象,而不是将转换延迟到 locate-source

  • 过程:(make-source-file-descriptor string binary-input-port)
  • 过程:(make-source-file-descriptor string binary-input-port reset?)
  • 返回:一个源文件描述符
  • 所属库:(chezscheme)

为了计算封装在源文件描述符中的校验和,此过程必须从 binary-input-port 读取所有数据。如果 reset? 存在且为 #t ,则端口被重置为其原始位置,就像通过 port-position 设置一样。否则,它指向文件结尾。

  • 过程:(source-file-descriptor? obj)
  • 返回:如果 obj 是一个源文件描述符则返回 #t ,否则返回 #f
  • 所属库:(chezscheme)

  • 过程:(source-file-descriptor-checksum sfd)

  • 返回:sfd 的 checksum 组件
  • 所属库:(chezscheme)

  • 过程:(source-file-descriptor-path sfd)

  • 返回:sfd 的 path 组件
  • 所属库:(chezscheme)

sfd 必须是一个源文件描述符。

  • 过程:(source-file-descriptor path checksum)
  • 返回:一个新的源文件描述符
  • 所属库:(chezscheme)

path 必须是字符串,checksum 和必须是精确的非负整数。此过程可用于构造自定义源文件描述符或从 path 以及 checksum 组件重构源文件描述符。

  • 语法:(annotation-option-set symbol ...)
  • 返回:一个用于注解选项的枚举集
  • 所属库:(chezscheme)

可以将 annotation-options 枚举集传递给 make-annotation,以控制注解是用于调试、分析,还是两者都使用抑或两者都不使用。因此,每个符号必须是 debug 抑或 profile

  • 过程:(syntax->annotation obj)
  • 返回:一个注解或者 #f
  • 所属库:(chezscheme)

如果 obj 是一个注解,则返回该注解;如果 obj 是一个封装了注解的语法对象,则返回被封装的注解。

  • 过程:(get-datum/annotations textual-input-port sfd bfp)
  • 返回:见下
  • 所属库:(chezscheme)

sfd 必须是源文件描述符。bfp 必须是一个精确的非负整数,并且应该是从 textual-input-port 读取的下一个字符的字符位置。

此过程返回两个值:带注解的对象和截止文件位置。在大多数情况下,第一次应该以 bfp 为 0 表示从文件开头调用 get-datum/annotation,并且对后续的调用来说,bfp 应该是前一次调用 get-datum/annotation 的第二个返回值。该协议对于处理包含多字节字符的文件是必需的,因为文件位置不一定对应于字符位置。

  • 过程:(open-source-file sfd)
  • 返回:一个端口,或者 #f
  • 所属库:(chezscheme)

sfd 必须是源文件描述符。此过程尝试查找并打开由 sfd 标识的源文件。它返回一个文本输入端口,如果成功则位于文件的开头,否则返回 #f。 当文件的校验和与 sfd 中记录的 checksum 不匹配时,即使其中一个源目录中存在具有正确名称的文件,它也会失败。

  • 过程:(locate-source sfd pos)
  • 过程:(locate-source sfd pos use-cache?)
  • 返回:见下
  • 所属库:(chezscheme)

sfd 必须是源文件描述符,pos 必须是精确的非负整数。

此过程使用先前的 sfd 请求中的缓存信息(仅当提供了 use-cache?,并且为 #t 时)或尝试查找并打开由 sfd 标识的源文件。如果成功,则返回三个值:字符串 path,精确的非负整数 line 和精确的非负整数 char,分别表示由指定的源文件描述符和文件位置表示的行内的绝对路径名,所在行和字符位置。如果不成功,则返回零值。当文件的校验和与sfd 中记录的 checksum 不匹配时,即使其中一个源目录中存在具有正确名称的文件,它也会失败。

  • 过程:(locate-source-object-source source-object get-start? use-cache?)
  • 返回:见下
  • 所属库:(chezscheme)

此过程类似于 locate-source,但它不是采用 sfd 和一个位置,而是采用源对象以及一个用于表示想要获取起始位置还是截止位置的布尔变量。

如果 get-start? 为真且 source-object 具有行和列,则此过程返回 source-objects 的 sfd,source-object 的行和列中的路径。

如果 source-object 没有行和列,那么这个过程用 source-object 的 sfd 调用 locate-source,而 locate-source 的第二个参数是采用 source-objectbfp 还是 efp,取决于 get-start?use-cache?

  • 线程级参数对象:current-locate-source-object-source
  • 所属库:(chezscheme)

current-locate-source-object-source 确定系统用于根据源对象报告错误的源位置查找函数。此参数对象最初绑定到 locate-source-object-object

可以通过调整此参数对象来控制从源对象中提取源位置的方式,除了 locate-source-object-object 之外,还可以使用记录的信息、缓存和文件系统等方式。

results matching ""

    No results matching ""