tutorial. Scheme解释器的多文件工程[lp-000A]

一个程序可以由多篇文章解释,也可以输出到多个文件。两种划分不必一一对应:解释器的求值规则、闭包与环境提供片段,本页把同一份定义组织成Guile模块。它不是复制出来的另一套解释器。

Scheme工程的文件与阅读关系[#63]

环境、原语、求值器、入口与测试各有模块边界。evaluate与apply-value互相调用,所以进入同一个engine模块;内部递归不变成两个文件之间的循环导入。阅读仍可分别讨论求值和闭包,不需要按文件目录排列。

(make-program "mce-primitives-module" "Scheme原语模块"
  (list primitives-fragment) '(initial-environment)
  #:imports (list (make-import '(lp-example mce environment)
                              '(extend-environment))))

make-import声明实际导入的模块与名称,make-module-output将计划映射到Guile模块路径。make-project核验项目内的导出、重复身份和模块顺序;它不自动排序,也不猜测自由变量。外部模块在实例化或独立运行时由Guile解析。

project. Scheme解释器的模块工程[mce-project]

Module order: 文件按显式依赖顺序声明;每个Module拥有独立的导入和导出。

module. Scheme环境模块[mce-env-module]

Outputs: lp-example/mce/environment.scm

Checks: Scheme工程的11项值与5项异常核验

Exports: (extend-environment lookup define-value!)

Initialization order: 片段按下列清单建立;函数体依赖可以相互引用。

module. Scheme原语模块[mce-primitives-module]

Outputs: lp-example/mce/primitives.scm

Checks: Scheme工程的11项值与5项异常核验

Imports: (lp-example mce environment) (extend-environment)

Exports: (initial-environment)

Initialization order: 片段按下列清单建立;函数体依赖可以相互引用。

module. Scheme求值与闭包应用模块[mce-engine-module]

Outputs: lp-example/mce/engine.scm

Checks: Scheme工程的11项值与5项异常核验

Imports: (lp-example mce environment) (extend-environment lookup define-value!)

Exports: (evaluate-sequence)

Initialization order: 片段按下列清单建立;函数体依赖可以相互引用。

module. Scheme解释器入口模块[mce-entry-module]

Outputs: lp-example/mce/entry.scm

Checks: Scheme工程的11项值与5项异常核验

Imports: (lp-example mce primitives) (initial-environment)

Imports: (lp-example mce engine) (evaluate-sequence)

Exports: (run)

Initialization order: 片段按下列清单建立;函数体依赖可以相互引用。

module. Scheme工程核验模块[mce-checks-module]

Outputs: lp-example/mce/checks.scm

Checks: Scheme工程的11项值与5项异常核验

Imports: (lp-example mce entry) (run)

Exports: (run-checks)

Initialization order: 片段按下列清单建立;函数体依赖可以相互引用。

Module的Exports是公开入口,Imports链接到提供名称的项目模块,Initialization order是定义建立顺序。Outputs直接打开生成文件。片段上的Assembled in列出所有使用它的计划,包含原来的单文件计划和本页的模块计划;这些信息来自装配数据,不需要另写索引。

Scheme工程的模块隔离与构建核验[#64]

instantiate-project为每个计划创建独立命名空间,只连接显式导出的binding;项目实例不会注册到宿主的全局模块表。不同项目实例有自己的本地定义,但外部导入的模块状态仍遵循Guile的共享规则。

(define instance (instantiate-project interpreter-project))
(define run (project-binding instance '(lp-example mce entry) 'run))
(run '((+ 1 2)))

核验案例复用11个值案例和5个异常案例的实际数据;输出的是读取时的数据快照,不导入Flora模块。这里核验多模块解释器本身,自解释仍由同源自解释案例检查。

program. Scheme工程复用的值与异常案例[mce-project-cases]

Defines: (examples errors)

Assembled in Scheme工程核验模块

Outputs: lp-example/mce/checks.scm

(define examples
  '((factorial
     ((define (factorial n) (if (= n 0) 1 (* n (factorial (- n 1)))))
      (factorial 6))
     720)
    (lexical
     ((define x 10)
      (define add-x (lambda (y) (+ x y)))
      ((lambda (x) (add-x 2)) 100))
     12)
    (closure
     ((define (make-adder x) (lambda (y) (+ x y)))
      (define add-three (make-adder 3))
      (add-three 4))
     7)
    (lazy-if ((if #t 7 missing)) 7)
    (truth ((if 0 1 2)) 1)
    (mapping ((map (lambda (x) (* x x)) '(1 2 3 4))) (1 4 9 16))
    (application ((apply + '(1 2 3))) 6)
    (named-let
     ((let loop ((n 5) (total 0))
        (if (= n 0) total (loop (- n 1) (+ total n)))))
     15)
    (let-star ((let* ((x 3) (y (+ x 4))) (* x y))) 21)
    (short-circuit ((list (and #f missing) (or 9 missing))) (#f 9))
    (conditional ((cond ((= 1 2) missing) ((= 2 2) 8) (else 0))) 8)))


(define errors
  '((missing (missing) mce-unbound)
    (arity (((lambda (x) x) 1 2)) mce-arity)
    (call ((1 2)) mce-not-procedure)
    (syntax ((if #t 1)) mce-syntax)
    (parameters ((lambda (x x) x)) mce-syntax)))

program. Scheme工程的独立核验入口[mce-project-checks]

Defines: (caught-key run-checks)

Assembled in Scheme工程核验模块

Outputs: lp-example/mce/checks.scm

Requires: run, examples, errors

(define (caught-key thunk)
       (catch #t (lambda () (thunk) 'returned)
         (lambda (key . arguments) key)))

(define (run-checks)
       (for-each
         (lambda (example)
           (unless (equal? (run (cadr example)) (caddr example))
             (error "Result mismatch" (car example)))) examples)
       (for-each
         (lambda (example)
           (unless (eq? (caught-key (lambda () (run (cadr example)))) (caddr example))
             (error "Exception mismatch" (car example)))) errors)
       (list (length examples) (length errors)))

example. Scheme工程的11项值与5项异常核验[mce-project-result]

(run-project-checks)

⇒ (11 5) · checked

program-check将已经核验的结果tree关联到被测计划,使各Module的Checks指向这项证据;它本身不增加执行,也不自动推断测试覆盖范围。

Scheme工程的源码下载与独立运行[#65]

五个.scm文件保留模块目录结构。Guile模块名(lp-example mce entry)对应lp-example/mce/entry.scm;生成文件中的define-module、导入和导出来自计划,定义来自原片段。下载单个文件并不包含依赖模块,独立运行需要保留这套目录。

在Flora根目录运行以下命令即可核验生成工程;这里不需要Grove或Flora模块:

guile --no-auto-compile -L build/programs -c '(use-modules (lp-example mce checks)) (write (run-checks)) (newline)'

成功输出(11 5),错误会以非零状态退出。也可用(use-modules (lp-example mce entry))取得run,用普通Scheme程序调用解释器。本实验面向Guile模块,不声称兼容其他Scheme实现。

LP依赖声明与初始化错误[#66]

Requires描述函数运行等阶段需要的名称;Initial requires描述整个片段开始建立时已经需要的名称。例如保存初始值的片段必须排在使用这个值的初始化片段之前:

(code-fragment "seed" "初始值"
  (define seed 3))
(code-fragment "answer" "初始化时读取初始值"
  #:initial-requires (seed)
  (define answer (+ seed 1)))

顺序写反时,诊断包含计划、片段、缺少的名称与来源位置。重复定义会指出两个提供者及其来源;导入与本地定义重名、项目内名称未导出也会被拒绝。Initial requires是作者声明的片段级屏障,不是自动分析器:同一片段内部的初始化顺序、未声明依赖和运行时异常仍由执行核验发现。

以下实际片段展示Initial requires如何链接到提供者;正确顺序的实例得到4:

program. LP初始化示例的初始值[lp-seed]

Defines: (seed)

Assembled in LP初始化依赖的装配计划

(define seed 3)

program. LP初始化示例的依赖值[lp-answer]

Defines: (answer)

Assembled in LP初始化依赖的装配计划

Initial requires: seed

(define answer (+ seed 1))

module. LP初始化依赖的装配计划[lp-initialization]

Checks: LP显式初始化依赖的结果为4

Exports: (answer)

Initialization order: 片段按下列清单建立;函数体依赖可以相互引用。

example. LP显式初始化依赖的结果为4[lp-initialization-result]

(instance-binding (instantiate-program initialization-plan) 'answer)

⇒ 4 · checked

错误顺序只构造计划就会被拒绝,不需要等到运行。这里捕获诊断以便阅读;普通构建中不捕获这个异常时,构建会失败:

misc-error: (#f "~A ~S ~S ~S ~S" ("Initialization dependency must precede fragment" "lp-reversed" "lp-answer" seed ((file . "/home/jinser/vie/writing/flora/plants/lp-000A.plant") (line . 83) (column . 6) (end-line . 83) (end-column . 32) (start-offset . 3916) (end-offset . 3942))) #f)

Scheme环境片段的语义解释[mce-env-semantics]

环境定义中的框架保存可变binding。闭包捕获框架链,define更新同一个框架,让递归函数找到自身。这里的解释关注对象语言行为;模块接口解释讨论同一份定义的工程用途,两篇解释不复制代码。

Scheme环境片段的模块接口解释[mce-env-engineering]

环境定义同时被单文件计划和多模块工程使用。在工程中,环境模块只导出extend-environment、lookup与define-value!;原语模块需要其中一个,求值模块需要三个。不同的导入范围不要求不同的源码副本。语义问题见环境的递归框架解释。