coding, photo, plant and demo

*昔ぼくが考えた言語

tech js 20130504 121330
5,6年前かJavaScriptのプロトタイプチェーンやスコープチェーンに関して
「なんでこんなに分かりづらいの?スコープチェーンだけで十分じゃね?」
「ruby使ってると、functionとかreturnとか、いちいち打つのが面倒なんだよ!」
と思って、自作言語をruby上で作ったことがあった。

その後、2,3年くらい前か、「男ならクリスマスツリーよりシンタックスツリー」というネタを12月にやろうとふと思い、その自作言語をCに移植を試みたのだけど、結局面倒くさくなって、というより完成が間に合わず途中で放棄。

その古代遺跡がつい先日発掘された。言語名はscope。
俺考古学の一環として、その解析をしたいと思う。

まずはソースコードを
https://github.com/mitsuman/scope
に上げた。

さて、まずはビルドしよう。手元のcygwinでトライ。
 $ make                                                                                                                             g++ -O2 -c test.cpp
test.cpp:1:40: cppunit/ui/text/TestRunner.h: No such file or directory
むむ、エラーか。
cppunitが要るらしい。
setup.exeで入れなおす。

$ make                                                                                                                           
g++ -O2 -c test.cpp
g++ -O2 -c lexer.cpp
g++ -O2 -c lexer.test.cpp
g++ -O2 -c parser.cpp
g++ -O2 -c parser.test.cpp
make: *** `context.o' に必要なターゲット `log.h' を make するルールがありません.  中止.
今度はヘッダがないと抜かす。
どうやらcommit忘れのようだ。
情けないぞ!と昔の自分に渇を入れてみたものの、もうこのlog.hはどこにもないのである。

しかしこういうことは遺跡調査にはよくあること。
context.cppを見る限り、どうやらLOGとLOGFというデバッグ用のマクロがあれば良いらしい。
空実装で良いだろう。
$ echo "#define LOG()                                                                                                                          #define LOGF(...)
">log.h

$ make                                                                                                                                         g++ -O2 -c context.cpp
g++ -O2 -c context.test.cpp
g++ -o test.exe test.o lexer.o lexer.test.o parser.o parser.test.o context.o context.test.o -lcppunit
./test.exe || echo ""
...............................................................................

OK (79 tests)
なんか知らんが通った。

テストケース(*.test.cpp)があるので、そこを見れば大体の構造は把握できる。
クラスは大きくLexer,Parser,Contextの3つがある。

使い方は、
  • ParserとLexerとContextをnewする
  • Lexerに(scopeの構文で書かれた)文字列をセットする
  • ParserにLexerをセットする
  • Parserのstatement()を呼ぶと、Lexerから取り出されるトークンをstatementの文法に沿って解釈し、木構造(Node)を返す
  • そのNodeをContextに渡して評価する
という流れのようだ。

主な使い方はcontext.test.cppを見ると分かる。

言語の特徴として以下が見て取れる。
  • 動的型付け
  • 分岐は参考演算子のみ
  • "s = { statement }"の様に、スコープコンテキストをオブジェクトとして変数に代入でき、これを連想配列やクラスの代わりにする

これが端的に現れているのが、下記のテストケース。
  TEST_STATEMENT(context, "f={a=1;b=3;}f.a", 1)
fにはJavaScriptで言うなら、
 f = {a:1, b:3}; f.a;

クラスはどう作るのかというと、
  TEST_STATEMENT(context_7, "a=${x=2;y=3;} b=a();b.x+b.y", 5)
スコープの頭に$を付ければ関数になる。その関数を呼べば、スコープオブジェクトが返ってくる。JavaScriptで書けば、
a = function() { return {x:2, y:3} }; b = a(); b.x+b.y;
か、普通はprototype使いたいし、こう書くかな。
a = function() { this.x = 2; this.y = 3; }; b = new a(); b.x+b.y;

じゃあ、普通の値を返す関数はどう書くのか?
スコープの中の最後のstatementが;で終わってなかったら、そのexpressionの値がスコープを評価したときの値となる。
  TEST_STATEMENT(function, "f=$(a,b){a+b};f(1,2)", 3)
セミコロンの有り無しで挙動が75度くらい変わるという、かなりプログラマに厳しい構文である。ここは素直にreturn文を導入したほうが良かったのではないか。

これでクラスっぽいものは作れたとして、継承はどうするの?
TEST_STATEMENT(inherit_0, "A=${x=2;y=3;}; B=A+${z=10;}; b=B();b.x+b.z", 12)
クラス同士を足せば継承と見做すようだ。
やってることは、
A = function() { return {x:2, y:3} }; B = function() { var a = A(); a.z = 10; return a; }; b = B(); b.x + b.z;
みたいな感じ。ちょっと違うけど。
内部的には、構文木同士を足すと新しい構文木ができる。

ここまでで一貫しているのが、とにかく予約語を増やさないようにしていること。
functionもreturnもvarもthisもforもwhileもcontinueもbreakも何もない。
とにかく、単純にしようと、覚えることを減らそうとしている。

しかし、継承した際に問題となるのが、親クラスのメソッドの呼び出し、Javaでいうsuperみたいなものがないと面倒が多い。そこは諦めて、__selfと__parentというプロパティをスコープオブジェクトに生やしている。
TEST_STATEMENT(self_4, "a=7;b={__self.a=2;k=__parent.a + 1;};b.k", 8)

おそらく当初はループも末尾再起でいいじゃん、と思っていたのだろうけど、ここでヒヨったのかScheme的なものと差別したかったのか、他にも変なプロパティを導入している。
TEST_STATEMENT(control, "a=0;n=0;{n=n+1;a=a+n;__self.__cont=n<10;};a", 55)
1から10までの合計を計算しているが、どうやら__contというプロパティが立っていたら、そのスコープの評価をやり直すというものらしい。他にも__breakも作ってあった。

だんだん意味不明感が強まってきたけど、おそらく、for文やwhile文が作りたかったら自分で関数として定義すればいい、と考えていた、と思う。例えば、下記のように書けるようになることを目指していたと思われる(それでも末尾再起で良いじゃん、という説もある)。
while = $(cond, &statement) { __cont = cond(); statement(); }
sum = 0; n = 10; while (n > 0) {sum += n; n--;}
(関数呼び出しの括弧やカンマが省略可能で、&を付けると強制的に関数扱い、の想定。現状の実装だと、そうなっていないし、そうすることは難しいけど)

こうなってくると矛盾が見て取れる。言語仕様を簡易にしようとしたのに、結局使えるようにしようとすると複雑になってきてしまったのだ。果たしてこれが、JavaScriptやLuaより初学者に優しいだろうか?では、覚えやすさという点は置いておき、速度ではどうだろうか?仮にJIT化するにしても、ループすらも組み込み構文でない時点で、最適化を相当頑張らないと他の同類の言語と比べて不利なのは目に見えている。JITを頑張るということは、JITコンパイル処理自体が重くなることを意味している。そうなると、v8のようにruntime profilerを組み込んでhot spot検出を行い、二段階のコンパイルが必要になるかもしれない。ひとつの複雑さという綻びが、より大きな複雑さという綻びを産んでしまうのである。馬力があればそれでも突き進めるが、前途多難である、というか時間がない。

こりゃいかんね、というか、改めてLuaとか言語仕様と実装にセンスあるよなあ、と感心する始末なのであった。

あと、書き忘れたけど大きな実装上の問題としてはGCが実装されていないです。
あくまで構文の実験とはいえ、せめてmark&sweepくらいは実装しとけばよかったのに!