構文オブジェクトの設計

R6RSでは、identifierオブジェクトはマクロ(syntax)の中でしか扱えないことになっているが、nmoshでは普通のオブジェクトとして普通のプログラムからも扱えるようになっている。要するに、可能な限り伝統的マクロに近い形で健全なマクロが書けるようになっている。

identifierオブジェクト

identifierオブジェクトはScheme的にはvectorであり、概念的には次のような構造をしている。

identifier := #(NAME <environment> <debug>)

つまり、identifierとは、schemeのsymbolに環境をくっ付けたものとなる。
cf. http://practical-scheme.net/wiliki/wiliki.cgi?Scheme:%E3%83%9E%E3%82%AF%E3%83%AD:CommonLisp%E3%81%A8%E3%81%AE%E6%AF%94%E8%BC%83

つまり末端のシンボルがidentifierに化けるのと、クオートされたリテラルに 環境が影響を与えないことを除いて、元のS式の構造はキープされるってことですね。

(R6RSでは、字面上のquoteが実際のquoteかどうかは不明なので、nmoshではクオートされたリテラルは最初無視して全部がプログラム的な意味のシンボルであると仮定している。psyntaxでは、”パターンマッチに必要な部分だけ変換Eをかける、という方法でこの問題を 回避”している。)
environmentは環境のlistであり、環境は下で説明するbindingオブジェクトのlistになっている。debugは、プログラムのシンボルが出現した位置を記録するためのスペースとなる。
nmoshでソースコードを読み込むと、ソースプログラム中のシンボル全てがdatum→syntaxによってこのidentifierオブジェクトに変換される。(これがnmoshがSRFI-38なソースコードを取り扱えない理由になっている。循環構造が有ると、単純な再帰的mapがおわらなくなる。readerが直接identifierオブジェクトを生成するのが好ましいように思える。)
R6RSのsyntax→datum手続きは、このidentifierオブジェクトからNAMEを取り出すだけの手続きになる。identifierでないものをsyntax→datumすると、pairなら再帰的にsyntax→datumを適用し、そうでなければそのままの値を返す。
ちなみに、シンボル以外はidentifierオブジェクトに変換されることは無い。逆に言えば、ソースコード中のシンボルは全てidentifierオブジェクトになっている。

bindingオブジェクト

bindingオブジェクトはexpanderにおける環境を表現する。環境はbindingオブジェクトのlistのlistとなっている。

binding := #([type = identifier-macro | macro | variable] source-name library-name export-name code)

bindingはmacroまたはvariableのtypeを持っている。現状(0.2.6)のnmoshでは更にpattern-variableも独立したtypeとして持っているが、これは廃止される予定。identifier-macroが独立したtypeになってしまっているのがあまり美しくない。。原理的にはmacroとidentifier-macroは同一でも問題は起こらないが、(... macro ...)のように、リストの先頭にマクロが来ないケースをエラーにできなくなるのでデバッグ上望ましくない。
source-nameは、bindingの"字面上の"名前(symbol)となる。exportはcore schemeとして渡す際のリネームされた名前であり、bindingの実質的な役割は、source-nameとexport-nameの対応を記録することにある。
library-nameはbindingを導入したライブラリの名前が入る。デバッグ用。
codeは、typeがmacroの場合だけ有効であり、codeに手続きが入っている(procedure?)ならcodeはマクロ変換器となる。codeが#fなら、そのマクロ変換器は"まだ生成されていない"事を示す。

expanderの動作

expanderは、基本的にこの2つのオブジェクトと、その構造であるソースコード(identifierのリスト)と環境(bindingのリスト)を元に動作する。
1. ソースコードの読みとり。まず、ソースコードを読み取り、identifierのリストに変換する。
2. ライブラリのimport。プログラムのimport節を読み取り、rootとなる環境を構築する。典型的なスクリプトなら(rnrs)をimportしているので、初期の環境は次のようになるだろう。

root = (
#(macro begin (rnrs base) begin~00 #<procedure core-expand-begin>)
#(macro define (rnrs base) define~00 #<procedure core-expand-define>)
#(procedure write (rnrs io simple) write~00 #f)
...)

最初に読み取ったコードには環境が含まれていないので、コードのrootにあたるidentifierオブジェクトを破壊的に更新する。
3. expand。expandの細かい動作は後で書くのでここでは概要だけ。

  1. pairならcar部を見る。
  2. car部がmacroなidentifierなら、codeに有る手続きを起動し、結果をまたexpandする。
    1. ただし、begin、define、define-syntaxだけは特別な処理を行う。(後述)
  3. car部がvariableなら、nameをキーとしてbindingを引き、bindingが有る場合: bindingのexport-nameのシンボルを出力する bindingが無い場合: エラー。
  4. そうでない場合、identifierかつvariableならbindingを引いてexport-nameを出力。それ以外(数値や文字列のようなself-evaluatingなオブジェクト)ならそのまま出力。

begin、define、define-syntaxは特別扱いされる必要がある。というのも、この3つの構文だけは、bindingが"カッコを越える"ため。
基本的にlambdaやletのような構文は、新たなbindingを導入しても、そのbindingがカッコの中を越えることは無い。
しかし、begin、define、define-syntaxは導入されたbindingがカッコを越える。
簡単な例。

(begin ;; ← beginの中で宣言されたaは、
  (define a 10))

(write a) ;; ← ここでも使える。

よって、これらの構文を展開するときだけは、操作する環境を一段階親の環境にしなければならない。
nmoshでは、これらの構文はsequenceとして特別扱いしている。R6RSの言う、defineの出てきて良いところをsequenceとし、sequenceの式全てに共有される環境を別に用意している。