syntax-rulesの実装方法について調べるために、FomentというR7RS処理系のソースを読んでみることにした。

せっかくなので、コードリーティングの際に何をしているかを実況のような感じで書いてみる。

ファイル構成

まず最初に、全体のファイル構成を把握する。それぞれのファイルをちらっとだけ見て、何が書いてありそうかをメモしておく。

Fomentの場合はファイル数が少ないので楽だった。

- src/ - main.cpp - base.scm - txt2cpp.cpp - license.txt - コア - compile.cpp - compile.hpp - execute.cpp - execute.hpp - foment.cpp - foment.hpp - gc.cpp - io.cpp - io.hpp - library.cpp - genpass.cpp - midpass.cpp - synpass.cpp - mini-gmp.c - mini-gmp.h - synrules.cpp - unicode.cpp - unicode.hpp - unidata.hpp - ライブラリ - chars.cpp - compare.cpp - filesys.cpp - hcontain.cpp - numbers.cpp - numbers.hpp - pairs.cpp - read.cpp - strings.cpp - syncthrd.cpp - syncthrd.hpp - vectors.cpp - write.cpp - test/ - テスト - unix/ - makefile - windows/ - makefile等 - LICENSE - README.md

src以下に全ての.cpp, .hppが入っているようだが、できればcoreとlibraryとかに分けてほしいな。

synrules.cpp

名前からすると、src/synrules.cppというファイルにsyntax-rulesのソースがありそうだ。拡張子がcppなのでC++だと思うが、わりと独自の世界観というか、普通のC++の書き方ではないようで面白い。

ファイル末尾で、構造体を定義している。構造体と言ってもCのstructというよりは、R7RS Recordの可能性が高そうだ。

R.SyntaxRulesRecordType = MakeRecordTypeC("syntax-rules", sizeof(SyntaxRulesFieldsC) / sizeof(char *), SyntaxRulesFieldsC);

Rというのは何だろうか。ちょっと検索がしづらいが、ソース内でグローバルに見える変数だろうから、foment.cppあたりを探してみると、以下の定義があった。

FRoots R;

Rの中身はsyntax-rulesの実装には直接関係ないような予感がするけれど、正体が分かっているものがたくさんあった方がソースは読みやすいので、ちょっと調べてみよう。

FRoots R

git grep FRoots でFRootsの定義を探すとfoment.hppにあった。このファイルの冒頭にはTODOリストがある。いちばん最初は1ファイルで開発していたのかもしれない。

FRootsは以下のような構造体だった。グローバルな変数の置き場なのだろうか。

// ---- Roots ---- typedef struct { FObject Bedrock; FObject BedrockLibrary; FObject Features; FObject CommandLine; 略... FObject DatumReferenceRecordType; } FRoots;

余談だが「// ---- Roots ----」のように要所要所に区切りが書いてあるのを見つけると嬉しくなる。単に見やすいだけでなく、コードを整理しようという意志が感じられるからだ。

FObject

ついでなのでFObjectの定義も確認してみる。が、単にvoid *のtypedefだった。へえ。

typedef void * FObject;

MakeRecordTypeC

synrules.cppの以下の行を見ていたのだった。

R.SyntaxRulesRecordType = MakeRecordTypeC("syntax-rules", sizeof(SyntaxRulesFieldsC) / sizeof(char *), SyntaxRulesFieldsC);

Rがどういうものかなんとなく分かったので、次はMakeRecordTypeCを調べてみる。これもfoment.cppに定義があった。

FObject MakeRecordTypeC(const char * nam, uint_t nf, const char * flds[]) { FObject oflds[32]; FAssert(nf <= sizeof(oflds) / sizeof(FObject)); for (uint_t fdx = 0; fdx < nf; fdx++) oflds[fdx] = StringCToSymbol(flds[fdx]); return(MakeRecordType(StringCToSymbol(nam), nf, oflds)); }

すぐ上にはMakeRecordTypeという関数がある。Cというsuffixは何かなと思ったが、第一引数の型が違うようだ。C++のCか、CharのCあたりだろうか。

FObject MakeRecordType(FObject nam, uint_t nf, FObject flds[])

FObject MakeRecordTypeC(const char * nam, uint_t nf, const char * flds[])

すぐ下では%make-record-typeという何かを定義している。「%」を頭に付けるのはSchemeのコードでわりと見られる命名なので、おそらくDefineというマクロを使うとSchemeレベルの関数が定義されるのであろう。

Define("%make-record-type", MakeRecordTypePrimitive)(int_t argc, FObject argv[]) { // (%make-record-type <record-type-name> (<field> ...)) FMustBe(argc == 2); SymbolArgCheck("define-record-type", argv[0]); ...略 return(MakeRecordType(argv[0], VectorLength(flds), AsVector(flds)->Vector)); }

R7RSにはdefine-record-typeという構文があるので、きっとそこからこの%make-record-typeが呼ばれそうな気がする。git grepでdefine-record-typeを探すと、src/base.scmにそのような定義があった。

(define-syntax define-record-type (syntax-rules () ((define-record-type type (maker arg ...) predicate field ...) (begin (define type (%make-record-type 'type '(field ...))) (define maker (define-record-maker type () () arg ...)) (define (predicate obj) (%record-predicate type obj)) (define-record-field type field) ...))))

Define

このDefineというのは構文みたいに見えるけど、C++にはこんな記法はないので、どこかにマクロ定義があるはず。きっとfoment.cppかhppだろう。

Define("%make-record-type", MakeRecordTypePrimitive)(int_t argc, FObject argv[])

hppの方だった。この例だとnameが "%make-record-type" 、primがMakeRecordTypePrimitiveに対応する。

#define Define(name, prim) \ EternalSymbol(prim ## Symbol, name); \ static FObject prim ## Fn(int_t argc, FObject argv[]);\ static FEternalPrimitive prim ## Object = { \ .ObjHdr.FlagsAndSize = OBJHDR_GEN_ETERNAL | sizeof(FPrimitive), \ .ObjHdr.TagAndSlotCount = (PrimitiveTag << OBJHDR_TAG_SHIFT) | 1, \ .Primitive.Name = prim ## Symbol, \ .Primitive.PrimitiveFn = prim ## Fn, \ .Primitive.Filename = __FILE__, \ .Primitive.LineNumber =__LINE__, \ .ObjFtr.Feet = {OBJFTR_FEET, OBJFTR_FEET} \ }; \ FObject prim = &prim ## Object.Primitive; \ static FObject prim ## Fn

Define("foo", FooPrimitive) のようにすると、以下のようなものが定義されるようだ。

EternalSymbol(FooPrimitiveSymbol, "foo");

FEternalPrimitive FooPrimitiveObject

FObject FooPrimitive = &FooPrimitiveObject.Primitive;

FObject FooPrimitiveFn

こうやって対象領域のために言語を拡張するのはLispプログラマらしい感じがする。

EternalSymbolの定義もfoment.hppにある。たぶん、Schemeプログラムの最初から最後まで存在するシンボルオブジェクトがEternalSymbolなのだろう。

今日のところはここまで。