式鬼言語航海日誌
categories/式鬼言語航海日誌2007-02-22
と、いうことで、久し振りに航海日誌を付けたいと思います。
「ほんと、久し振りだね。」
まあ、式鬼言語の実装を始めて既に2年。コンセプトも実現手段も当初のアイディアからかなり変わりましたけど、とりあえずはプロセスが回るところまで来ましたからね~~
「まだまだライブラリ設計とかエラー処理とか、コンセプトすら固まっていない重要項目はいくらでもあるけどね」
ま、まあ、そのあたりはぼちぼちやるとして……考えてますよ? ちゃんと。
でも、やっぱりプログラム言語の設計は難しいですからね。忙しいのにかまけて手を付けないことが多くなってきたので、ここは日誌をつけて少しはやる気を出そうかと。
「いつまで持つのやら」
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-02-26
参考文献書き始めました。
「とりあえず参考にした言語の解説だけかい」
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-06-08
ただいま~~ようやく戻ってこれました……。
「……またまたずいぶん時間が空いたねぇ」
まあ、世を忍ぶ仮の仕事が忙しかったということもあったのですが、言語設計の方もドツボにはまってまして……ようやっと抜け出したところです。
「おやおや」
不用意に実行プロセスの意味論を追っ掛け始めたのが運の尽きでしたね。もう泥沼に嵌りこむ感じです。Forthの奥深さに触れて調子に乗っていたところもあったのでしょうけど。
「というと?」
Forthには、immediateという仕組みがあって、Word一つ一つにソースコードの解釈時に実行する内容を定義することができるんですよね。No.22さんのところの解説とか、Naoさんのmops immediateの解説に解説がありますが、このimmediateという仕組みによって、マクロ的な内容を通常のWord定義の範疇で行うことができます。
「ミニマリストのあンたの好きそうな話だね」
ええ、こういうの大好きです。
ただ、Forthの場合、あくまで『Wordの定義』というパースの基本ルールに従う必要があるので、『Wordをスペースで区切らなくてはいけない』などのルールには従う必要があるようです。No.22さんのところで文字列リテラルの例が挙げられていますが、文字列リテラルをスタートする『s"』もWordで定義しているため、『s"』直後にスペースを置かなくてはならないという制限が発生しています。
「判りゃ大した話じゃ無いけど、あんたの嫌いそうな制限事項だね」
ええ、そうですね。汎用化という点からすると何とも嫌らしい制限ですよね。
まあ、Forthの場合、あくまでWord定義を変更するだけでパーザまで弄れる訳ではないですから、当然といえば当然ですが。
ただ、ここまで弄くれるのなら、とことんまで弄れるようにしたい……だったら、パーズ中に文法定義を修正できるようにすればいいじゃないか、と。
「おお、『万能チューリングマシン』……かね?」
そうですね。たぶんイメージ的には『万能チューリングマシン』に近いものかも。
ソースコードをパーズしながら実行して、場合によってはパーザ自体の修正も行う。マクロの発展版ですね。
ただ、これを実行プロセスとどうやって擦り合せるか……そこで泥沼に嵌っていました。結局、パーズそのものを実行プロセスの一環にしてしまいましたけどね。
「なンだそりゃ。ソースのパーズする部分をグローバル変数か何かに割り当てたンかい?」
まあ、そんなもんですね。
簡単に言うと
『ソースコードをパーズするオブジェクト』を別途用意する。このオブジェクトには常にアクセスできるようにする プログラムの実行プロセスとして、このオブジェクトにソースコードをパーズさせる 適当なタイミングで、パーズした内容(構造木)を実行する
といったところですね。『パースする <--> 実行する 』といった感じで交互に処理しますので、実行しながらパーザの定義=文法を入れ替えることができる……というわけです。
「ほう、そりゃ凄そうだね。でもセキュリティ確保とかバグ取りは大変そうだ。ライブラリをインクルードしただけで酷いことになる可能性があるわけか…………」
ええ、そのへんはマクロとかevalと一緒ですね。下手に弄るとトンでもないことになるでしょう。
まあ、そこは『膝を撃ち抜く自由』というやつで。
「……『足を滑らせる自由』のような気がしてならんけど」
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-09-02
まいど。夏の盛も過ぎて、すっかり涼しくなりましたね。
「前は初夏も始めだったのにねぇ」
まあ、期間がかかりましたが進捗ありましたよ。基本的なメソッド呼び出しまで意味付けできました。
忘れないうちにメモ書きしておきます。頭の中を整理するために書いていますので、判り辛いのはご容赦下さい。
「図も使うと分かり易いンだけどねぇ」
そこはこのWikiエンジンの限界ということで……
まず、全体の流れとしては RootとなるCellに文字列を与える。 Rootが文字列をパースして、命令列に変換 Rootの中のパースエンジンが文字列を命令列に変換してRootのhead列に保存 トップレベル(=Root)のスコープで空行が出て来たらパースを中断 パースを中断したタイミングで、解釈済の命令列を実行 Rootのhead列を逆さにしながらbody列に全て移動 body列の命令を実行 body列が無くなったら実行を中断してパース再開 上記を繰り返し 文字列をパースしきったら終了
といった感じです。6月のアイディアを具体化したものですね。
「ブロックごとに実行していく感じかね」
そうですね。塊ごとに逐次的に実行していく感じですね。
まだ実装はしていませんが、究極的にはRootのパースルールを変更する命令列も用意したいと考えています。
で、次が命令列の挙動について。
命令列……式鬼言語ではCellと呼んでいますけど、大きく分けると
トークン(id): 名前空間から変数を持って来て戻り値として継続のhead列に(戻り値として)渡したあと、 メソッド呼び出しをスタートする。 オブジェクト:自分自身を戻り値として継続のhead列に(戻り値として)渡す。 その他命令列:それぞれ固有の処理を行う。戻り値も色々。
といった内容になります。
「判り辛いねぇ。色々と前提をすっ飛ばしているだろ。この説明」
……そうですね。まあ、まずはメモ書きということで……オブジェクトとその他命令列はまあ、何とかわかるのではないかと思います。手続き型言語でいうと、オブジェクトはリテラルの、命令列はコマンドの、それぞれの挙動に近いですからね。
で、最後のトークン(id)は、まとめて言うと、
変数の名前解決 --> 変数に保存されているCell(or トークンそのもの)を利用してメソッド呼び出しを処理
というCellということになります。
「さっぱり判らん。例を出せ、例を」
そうですね。まあ、実際の例を出しても判らないかもしれませんが……
:test1 :test2 .. :test3 <== {!'test'} test1 test2 .. test3
というソースを例に説明しましょう。
まず、Root(というCell)は、この文字列を途中までパースします。
|Transport| test1 |Transport| test2 .. |Transport| test3 |Skewer| |Transport| <InlineExecutor> |Copier| test1 test2 .. test3
|~~|というのは命令列のCellです。ここでは
- |Transport|:次に実行するはずの命令列を実行せずにそのまま継続に(戻り値として)渡す
- |Skewer|:メソッド呼び出しのベースになるCellを検索するして継続に(戻り値として)渡す
- |Copier|:継続に(戻り値として)積まれているCellを2つ取り出して、片方の内容をもう片方にコピーする
という挙動になります。
<InlineExecutor>というのも命令列なのですが、「{!~~}の中身を実行する」という命令列になります。
つまりは、「test1 test2 test3というCellをそれぞれ継承元とする引数を持つメソッドを定義する」という命令列になります。
命令列はRootのbody列に積まれていますので、順次実行していくと
Root::body |Transport| test1 |Transport| test2 .. |Transport| test3 |Skewer| |Transport| <InlineExecutor> |Copier| Root::tail
Root::body |Transport| test2 .. |Transport| test3 |Skewer| |Transport| <InlineExecutor> |Copier| Root::tail test1
Root::body .. |Transport| test3 |Skewer| |Transport| <InlineExecutor> |Copier| Root::tail test1 test2
Root::body |Transport| test3 |Skewer| |Transport| <InlineExecutor> |Copier| Root::tail test1 test2 ..
Root::body |Skewer| |Transport| <InlineExecutor> |Copier| Root::tail test1 test2 .. test3
Root::body |Transport| <InlineExecutor> |Copier| Root::tail <test1 test2 .. test3 メソッドのベースとなるCell>
Root::body |Copier| Root::tail <test1 test2 .. test3メソッドのベースとなるCell> <InlineExecutor>
Root::body Root::tail <test1 test2 .. test3メソッド>
といった感じです。
「ここで文字列のパースが再開されるわけだね」
はい。Rootのbody列を使い切りましたので、再開します。パースする前に、tailに積まれたCellはクリアされます。
パースが終了するとこんな感じですね。
Root::body test1 test2 .. test3 Root::tail
「そのまンまだね」
はい。ただ、bodyに積まれたのは命令列ではなくてトークン(id)ということに注意してください。では逐次実行してみましょう。
Root::body (|Trigger|) test2 .. test3 Root::tail test1
ここで|Trigger|という命令が新たに積まれていることに注意してください。()で括られているのは、実際にはbodyに積まれないで直接実行されるためです。
|Trigger|という命令は tailに積まれたCellを新しく積まれた順に検索してメソッドを探す 該当するメソッドが存在する場合はメソッドを実行する 該当するメソッドが存在しない場合は何もしない
という命令です。いわゆるメソッド呼び出しですね。
「ふむ……メソッドを名前で呼び出すンじゃなくてCellで呼び出す、つうことか」
はい。トークンを処理するときはその都度メソッド呼び出しが発生します。パフォーマンス的なインパクトはありますが、代わりに柔軟性が手に入ります。まあ、それは後ほど。
test1 test2 .. まではメソッドがありませんので、そのままtail列に積まれます。test3まで積むと、先ほど定義したメソッドが要求するCellとスタックに積まれたCell(の継承元)が一致しますので、ようやっとメソッドが実行されます。
Root::body (|Trigger|) Root::tail test1 test2 .. test3
Root::body (<test1 test2 .. test3メソッド>) Root::tail test1 test2 .. test3
メソッドは'test'という文字列を返します。
Root::body Root::tail test1 test2 .. test3 'test'
で、めでたく終了しました。
「長かったねぇ。結局、引数は使わなかったね」
ええ、まだ実装が追い付いていないところがありまして……
引数については
- tailに積んである引数を、順番入替え/コピー/削除する命令列がある
- tailから降ろす/参照するときに、Closure(Delegater)の評価を行う
という特徴があります。このあたりは実装が終わったころにもう一度ということで。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-09-03
それでは昨日の続きです。
「昨日といっても日付が変わったばかりだけどね」
今回はメソッド適用です。
式鬼言語ではCLOSに倣ってマルチディスパッチになっています。
どのメソッドを呼び出すかは、スタックに積まれたCellが何かによって決まります。
下記が、今のところ唯一実装しているメソッドですけど、
'test1' 'test2' .. append
このように
- 引数となるCellをスタックに積む
- 引数の数を表すトークンを積む
- メソッドを呼ぶトークンを積む
ということを行います。
実際に動かしてみると
Root::body 'test1' 'test2' .. append Root::tail
Root::body (|Trigger|) 'test2' .. append Root::tail 'test1'
(省略)
Root::body (|Trigger|) Root::tail 'test1' 'test2' .. append
Root::body (String::append(スタックのCellを4つ使用)の命令Cell) Root::tail 'test1' 'test2' .. append
Root::body Root::tail 'test1test2'
となります。
ここでのポイントは「..」というトークンで、このトークンによって引数の数が一意に決まります。
「点の数がその後ろの引数の数、つうことだね」
はい、そうです。
あと、appendを実行するときにも色々と工夫がしてあります。
「そりゃなンだい?」
といっても、大したことではないですが……遅延評価用に、tail列からpopするとき、あるいはtail列のCellを参照するときに処理を行うようにしています。いわゆる遅延評価のforceですね。
Root::body (String::append(スタックのCellを4つ使用)の命令Cell) Root::tail 'test1' 'test2' .. append
Root::body Root::tail 'test1' 'test2' .. append # 2つは要らないので捨てる
Root::body Root::tail 'test1' 'test2' # 'test2'をpopするときにforceを実行 --> 'test2'のまま戻す
Root::body Root::tail 'test1' # 'test1'を参照するときにforceを実行 --> 'test1'のまま戻す String::append中 'test2'
Root::body Root::tail 'test1' String::append中 'test2' ('test1') # 2つをマージ
Root::body Root::tail 'test1test2' String::append中 'test2' ('test1test2')
Root::body Root::tail 'test1test2'
といっても、Stringはforceしてもそのままですので、あまり意味はありませんが……
「Lazyとして造ったCellを遅延評価で使うつうわけだね。移譲とかClosureでも使えるかな?」
そうですね。ただ、実装はこれからですけど……
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-09-03
ただいま戻りました。
「おお、今日は早いね」
ええ、昨日の話をちょっと考えていたので、仕事を早めに切り上げて帰ってきました。
「というと?」
昨日、
『トークンを処理するときはその都度メソッド呼び出しが発生します。パフォーマンス的なインパクトはありますが、代わりに柔軟性が手に入ります』
という話をしたかと思うのですが、────ホントにパフォーマンスを犠牲にしてまでそんな柔軟性が必要なのかと。
「ふむ────トークンは“メソッド呼び出し”と“変数の参照”を処理するっつうてたよね。それが不要っつうことかい?」
それぞれの機能は当然必要なのですが、それはトークンを処理するごとに行う必要があるのか、ということです。個々のパフォーマンスインパクトは小さくても、トークン処理はかなりの回数行うので。
それに、“メソッド呼び出し”の柔軟性は、リテラルを見てもらえばわかりますが、実はかなり限定的なんですよね。
「リテラルというと」
'test1' 'test2' .. append
はい、ここの“..”ですね。ここで引数の数を指定しているのですけど、結局のところ『ここでメソッドを呼びますよ』といっているようなものですからね。
「トークンを呼び出すたびにメソッドを実行するかどうかを判定しているけど、実は実行するポイントは決まっている、つうことか」
そうです。ということで、引数の存在するメソッドについてはあまり意味がありません。
「そうすると、効果があるのは引数なしのメソッドぐらいか」
はい。ただ、式鬼言語ではCLOSスタイルのメソッド呼び出しを考えていますので、実は引数無しのサンクというのは全然重要じゃないんですよね。メソッドを実行するときはほとんどの場合で(スタックに積まれている)Cellを必要とすることになりそうです。
あとは、メソッド呼び出しと同じリテラルでクロージャー呼び出しをするようにすることでカスタマイズポイントを提供するという技法もありますが、これも遅延評価に対応するとあまり嬉しくないですね。結局クロージャー呼び出しが必要になるのは引数の評価のタイミングですから、遅延評価でだいたいカバー出来てしまいそうです。
「“変数の参照”の方は?」
Forthライクの処理ですから、ほとんどがスタックだけで完結します。そもそも変数を参照しないんですよね。あるとしてもスコープを跨いだ参照とかの限定的なケース。それだったらわざわざトークン全てでやる必要は無いかと。
「ふうん。そこまで割り切るンかい。 で、どうする?」
はい。ここもForthチックに処理することにしましょう。メソッド呼び出しと変数呼び出し用のトークンとリテラルをそれぞれ別に用意します。具体的には
- ..:token メソッド呼び出し用のトークン。
- @token 変数呼び出し用のトークン
といった感じですか。ちょっとこれから実装してきます。
(追記)
ただいま戻りました。
「お、どうだった?」
とりあえずはメソッド呼び出し用のトークンのところまでは実装が終わりました……というか、ちょっとルールを変更して、
<|:test1 :test2 :..test3> {|'test'} test1 test2 ..test3
<| >という記法を導入しました。これは
- test1 test2 ..test3というメソッド呼び出しのベースになるCellを検索する
- 最後のトークン(この場合は..test3)をメソッド呼び出し用のトークン(Messageのインスタンス)にする
という処理を行う記法です。
「..test3というのが、さっき言っていたメソッド呼び出し用のトークン、つう訳だね」
はい、そうです。といっても、別に..test3じゃなくてtest3でも動きますが……。しかし、ここは表記ルールとして、トークンの頭に付く点の数で引数の数を区別することにしましょう。
変数呼び出し用のトークンはまた明日。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-09-08
ちょっとメモです。
コイツのせいで一日丸々無駄にしました……
「何やったンだい、一体」
いや、プログラムを組んでいて急に HEAP[test.exe]: Invalid Address specified to RtlFreeHeap というエラーが出て来るようになったので、一体全体どうしたのか、と。
「ほお」
まあ、どこかでアクセス違反でもしているのかと思いましたが、それにしてもおかしい。第一エラーが発生しているのはboost::xpressiveの奥の方。
最初はboost::xpressiveの使い方でも失敗したのかと思ったのですが、実際は全然関係なくて、コンパイルオプションの設定ミスでした。
“C/C ”の“プリプロセッサの定義”でDEBUGを指定しているにもかかわらず、“コード生成”の“ランタイム ライブラリ”をマルチスレッド(/MT)に指定してしまっていたんですね。
あとは上記のリンク先にある通りです。式鬼のソースはDEBUG指定をベースにコンパイルされているにもかかわらず、boost::testで使用するライブラリは“libboost_unit_test_framework-vc80-mt-s-1_34_1.lib”(マルチスレッド版)になってしまい、メモリの解放でエラーが発生……
「あらら、マヌケだねぇ」
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-09-09
ということで、今日はFiberを実装してみました。全てのプロセスのベースにするつもりですので、けっこう重要なところですね。
「ふぁいばあ?」
マイクロスレッドとかコルーチンとか言われるものですね。(ルーチンが自発的に)処理を一時中止することのできる特殊なルーチンです。
- Wikipedia:コルーチン
- 魅力的なPython: Pythonジェネレーターで「無重量スレッド」を実装する
- 東方弾幕風:マイクロスレッド講座
- Schi Heil と叫ぶために:プロセス、スレッド、ファイバ、タスク、ジョブ、違いを整理してみよう
あたりですかね。意外と解説が少ないようです。
関数型言語ですと「継続」を扱えると色々と便利ですが、構造化プログラミングですとコルーチンが使えると色々と便利になります。関数オブジェクトとかクロージャとかと組み合せると強力ですね。また、プロセスやスレッドなどと比べて呼び出すときの負担が軽いというメリットがあります。
「ふうン」
で、式鬼言語では、このFiberを実行の基本的な挙動にしようかと考えています。で、プロセスやスレッドはFiberから派生させる形で実装する……と。現にメインプロセスはFiberから派生させて実装しました。
詳細はこれから詰めですが、なかなか良い感じです。
「いっそのこと、クロージャとかデリゲータとかもこいつで作っちまうのも良さそうだね」
とりあえず使ってみましょう。
{!'test'}.execute
{!~~}の部分がFiberです。Fiberは.executeというメッセージを送り付けて実行することができます。
ここでは簡単に文字列'test'を戻すというFiberですね。
と、言っても、Fiberは元のプロセスからは切り離されます。戻り値はFiber自身が持っていて、呼び出し元のプロセスには戻しません。
そのため、このソースを実行すると、
- Rootの受け取った戻り値: Fiber
- Fiberの受け取った戻り値: 'test'
ということ結果になります。ですので、Fiberの実行結果を使いたいと思ったら、Fiberに.executeを送った後に、Fiber自身から結果を引っ張り出す必要があります。
「ちょっと面倒だねぇ」
まあ、派生型として呼び出し元に戻り値を返すFiberも実装しようかと考えていますので、必要だったらそっちを使うことにしましょう。
ついでに言うと、Fiber自体は破壊的実行です。一度実行したらそれでお仕舞いです。
例えば、
{!'test' ..append} 'test2' ..pack .execute 'test3' ..pack .execute
というソースがあったとします。これは
- Fiberに'test2'という文字列をFiberのスタックに積んで一度実行し、次に'test3'という文字列をFiberのスタックに積んで再度実行する
- Fiberは、スタックに積んである文字列に'test'という文字を結合する
という意味です。
Fiber自体は一度実行されたらお仕舞いですので、これは 'test2'に'test'を結合して'test2test'にする 二回目の.executeでは、Fiberはすでに終了しているので何もしない。'test3'はそのまま
という結果になります。こんな感じですね。
Root::tail (Fiber) (Fiber)::tail 'test2test' 'test3'
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-09-10
ちょっと考えてみたんですけどね。
「なンのことだい?」
Fiberの件で、『Fiberは元のプロセスからは切り離されます。戻り値はFiber自身が持っていて、呼び出し元のプロセスには戻しません』としている部分です。これはこれで一理ありますけど、プロセス側でFiberの結果を取り出さなくてはいけないとなるとちょっと面倒ですね。
「そりゃそうだ。遣りっ放しにできないからね」
普通なら呼び出し元のプロセスとFiberの間に継続の関係を用意して、Fiberの戻り値を元のプロセスで受け取るようにするところかと思いますが、これだとFiberと呼び出し元のプロセスとの依存関係が強くなってしまいます。
「まあ、それが普通だからね」
でも、せっかくFiberで独立性を高くしたのですから、それは避けたいところです。
なら、Fiberの引数として呼び出し元のプロセスを参照できるようにすればいいんじゃない? というアイディアを思い付きました。そうすれば、Fiber側で必要に応じて戻り値を元のプロセスに戻すかどうかを決めることができます。
つまり、Fiber .executeするときは、その第一引数として呼び出し元の継続を渡すようにしました。
「ふうん?」
簡単な例を挙げると、
{!'test'}.execute
のFiberが実行されるときは、その第一引数としてRootが渡されるということですね。
「Rootに戻したい時は、第一引数のスタックに値を積む、つうことか」
はい。こんな感じにします。
{!'test' ..pack}.execute
あと、Fiberに簡単に値を渡す方法が無いのは不便なので、引数を設定するとFiberに渡されるようにしました。
これも昨日の例をベースにして
{!'test' ..append} 'test2' ..execute 'test3' ..execute
といった感じです。
ここでは
- 一回目の..executeのメッセージによって、Fiberが(Root, 'test2')という引数で実行される。
- 二回目の..executeのメッセージによって、Fiberが(Root, 'test2test3' 'test3')という引数で実行される。
という挙動になります。
「まあ、何というか……判り辛いねぇ」
また、渡される継続は呼び出し元の継続なので、
{!{!'1' ..append ..pack} '2' ..execute ..append} .execute
というソースの場合、内側のFiberには一つ外側の(呼び出し元の)Fiberが第一引数に渡されます。
ですので、Fiberの中で実行される..packはRootではなく外側のFiberに結果を押し込みますので、
Root::tail (Root) (外側のFiber) (外側のFiber)::tail (Root) (内側のFiber) '21'
という状態になります。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-09-15
今日は条件分岐──if文ですね。
「といっても、まともな実装じゃないンだろうねぇ」
まあ、せっかくForthベースにしていますからね。式鬼言語の条件分岐は、
- 三項演算子による表記
- メッセージで実行されるメソッド
という特徴があります。
三項演算子はCとかで使われているアレですね。こんな感じの記載になります。
true ? 'true' ! 'false' true ? 'true' # 肯定のみ実行 false ?! 'false' # 否定のみ実行 'test' ? 'true' # 普通のCellはtrueと同じ挙動
'true'、'false'の部分は1つの塊である必要があります。ブロックか条件分岐かということですね。
条件分岐の連結にも対応しています。
true ? true ? 'tt' ! 'ft' ! true ? 'ft' ! 'ff' # 'tt'になる
「こうなるとさすがに判り辛いな」
で、条件分岐の挙動についてですけど、これは実際の挙動を見てもらった方が早いですね。
# true ? 'true' ! 'false' Root::body true ?! (肯定時の命令列を持っているCell) (否定時の命令列を持っているCell) Root::tail Root
Root::body ?! (肯定時の命令列を持っているCell) (否定時の命令列を持っているCell) Root::tail true Root
Root::body (肯定時の命令列を持っているCell) (否定時の命令列を持っているCell) Root::tail (true <- ?!) # tailの一番上に積まれたCellに?!というメッセージを飛ばす Root
Root::body 'true' # 肯定時の命令列を持っているCellの中身を展開 # 否定時の命令列を持っているCellを削除 Root::tail Root
実行する前の命令列を直接弄ることのできるForth系の特性を活かした実装ですね。Lispなんかですとマクロとかで実装するのですけど、Forthではそんなことする必要ありません。
「Forthでも暗黒魔法扱いされたりするけどねぇ」
否定条件を実行するときは、falseというCellに?!を飛ばします。
falseは?!のメッセージに反応して、通常のCellとは反対の
- 否定時の命令列を持っているCellの中身を展開
- 肯定時の命令列を持っているCellを削除
という動作をします。
# false ? 'true' ! 'false' Root::body false ?! (肯定時の命令列を持っているCell) (否定時の命令列を持っているCell) Root::tail Root
Root::body ?! (肯定時の命令列を持っているCell) (否定時の命令列を持っているCell) Root::tail false Root
Root::body (肯定時の命令列を持っているCell) (否定時の命令列を持っているCell) Root::tail (false <- ?!) # tailの一番上に積まれたCellに?!というメッセージを飛ばす Root
Root::body 'false' # 否定時の命令列を持っているCellの中身を展開 # 肯定時の命令列を持っているCellを削除 Root::tail Root
といった感じですね。
「しかし、妙に重いねぇ。条件が複雑になると途端にダメダメになるね」
そうですね──パーザーが悪いのかな? ちょっと効率化してみよう……
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-09-23
今回はブロックの呼び出しを実装しました。メソッド呼び出しとか関数呼び出しとか呼ばれるものですね。
「あれ? もう実装しているンじゃなかったっけ?」
ええ、そうなんですけど、ちょっと厳密に見直しました。以前にも書いていますけど内容が変更になっています。
例えばこんな感じですね。
<?:String :String :..test> <::{2 1 0| ..append}. #メソッドの定義 'test1' 'test2' ..test #メソッドの呼び出し。結果は 'test2test1'
それぞれの意味は
- <?:[引数のmaster] [引数のmaster]…… [メッセージ]>
[引数のmaster]をmasterとして持つCellがスタックに積まれている状態で[メッセージ]を飛ばした時に実行するCellをスタックに積む。 - master: 移譲先のCell。親クラスみたいなもの
- 頭に"."の付いた単語: スタックに積まれたCellに関連付けられたCellを取り出して実行するメッセージを実行するCell。"."と":"の数によって引数となるCellの数が変わる。
- [コピー先ブロック] <: [コピー元ブロック]
[コピー元ブロック]を実行した結果のCellを[コピー先ブロック]を実行した結果のCellにコピーする - :[Cell]
Cellを実行しないでそのままスタックに積む - {[引数の数] [引数1] [引数2]……| [実行するCell]……}
Executorを作る。Executorが実行されると スタックの上から[引数の数]分のCellを[引数1] [引数2]……の順番に並び換える [実行するCell]……を次に実行する命令列に押し込む という動作をする
- .
スタックの最後のCellをPopする - (空行)
パースを一時中断し、今までパースした結果を実行する
となります。
「細かいねえ。構文として処理しないで一つ一つの単語に意味を持たせた訳かい?」
はい。せっかくForthもどきにしているのですから、ここらへんも作り込んだほうが良いかと思いまして。ただし、タイプミスで全然違う挙動をするリスクがありますので危険でもありますが……まあ、ここはピーキーな俺言語ということで……
結局のところ、この文によって『スタックに文字列が2つある場合に、スタックの上の文字列にその下の文字列を加える』メソッドが定義されました。また、最後にpopしていますので、スタックには何も残りません。
「最後の“.”を忘れるとどうなるンだい?」
<:は最後に[コピー先ブロック]を残しますので、最後のpopがなければそのまま残ります。場合によっては次のパース実行で変なイタズラをする可能性がありますので、忘れずに処理するようにしてください。大抵は「メソッドを呼んでもいないのに実行される」という挙動になるかと思います。
「うわ……極悪……」
まあ、ここは(空行)でスタックの中身を空にするようにしようか悩み中なんですけどね。さすがにパーサーも絡んだ挙動は判り辛いですし。そのあたりはもうちょっと練り込むことにしましょう。
メソッド呼び出しは今まで使ってきたのと同じですね。
'test1' 'test2' ..test #メソッドの呼び出し。結果は 'test2test1'
文字列はそのままスタックに積まれ、..testメッセージによって先ほど定義したメソッドが呼ばれます。メソッドは前述の通り「スタックのCellを積みなおす」「メソッドの中身を次に実行する命令列に押し込む」という2つの動作を行うExecutorになっています。結果として、スタックに積まれた'test1''test2'の順番を入れ替えて、test2'に'test1'の文字を追加して、追加した文字列のみスタックに残します。
「Executorというのはある意味Forth Wordみたいな感じかね」
Forthの関数定義みたいな感じですかね?
いやあ、Forthって凄いですね。スタック&辞書(Forth Word)で、基本的な動作が破綻なくできてしまいます。
「でも、これじゃインラインで展開されるから名前の汚染とかフロー制御とか面倒そうだね。新しくスコープを作るのはないンかい?」
そのようなケースはFiberを活用します。こっちの説明はまた明日ということで。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-09-23 その2
ちょっと寄り道してSDLのインストール中です。手順をメモ書きします。
環境はVisualStudio2005 Standard SP1です
- Microsoft DirectX ダウンロードからDirectX SDK - August 2007をダウンロードしてインストール
- ついでなのでMicrosoft DirectX 9.0 Update (October 2004) 日本語ドキュメントもインストール
- SDLサイトのダウンロードページからSDL version 1.2.12 (stable)をダウンロード。せっかくなのでソースコードからコンパイルします。
- ダウンロードしたファイルを展開したフォルダにあるVisualC.zipをさらに展開
- 新しくできたVisualCというフォルダのSDL.slnを実行
- VCのプロジェクトのプロパティに、DirectX SDKのパスを追加
- 構成プロパティ - C/C - 全般 - 追加のインクルードディレクトリに C:\Program Files\Microsoft DirectX SDK (August 2007)\Includeを追加
- 構成プロパティ - リンカ - 全般 - 追加のライブラリディレクトリに C:\Program Files\Microsoft DirectX SDK (August 2007)\Lib\x86を追加
- 構成プロパティ - ライブラリアン - 全般 - 追加のライブラリディレクトリに C:\Program Files\Microsoft DirectX SDK (August 2007)\Lib\x86を追加
- なんかランタイムライブラリの設定がおかしいので修正
- Debug構成のコード生成-ランタイムライブラリをマルチスレッドデバッグDLLに修正
- 一応、出力ディレクトリ/中間ディレクトリを$(ConfigurationName)に変更
- ビルド- バッチビルドで一気にコンパイル
- VisualC/SDLとVisualC/SDLmain以下に成果物ができるので、これを適当なフォルダにコピー
- 元からあったincludeフォルダも一緒に適当なフォルダにコピー。他のサンプル例も参考に、include/SDLというフォルダ構成になるように修正。
次にプロジェクトを新規に作成しましょう。
- VC を立ち上げて新規プロジェクトを作成
- Win32プロジェクトを選択
- アプリケーションの設定では追加のオプションに空のプロジェクトを選択
- とりあえずはmain.cppとmain.hppを追加
- プロジェクト-プロパティの設定
- C/C
- 全般 - 追加のインクルードディレクトリに先ほどSDLをインストールしたパスを入力
- コード生成-ランタイムライブラリを適切なものに修正
ここでは/MTdと/MDdのどちらかにします。静的リンクにチャレンジしてみましたが上手く行きませんでした。そう簡単では無いようですね。 - リンカ
- 全般-追加のライブラリディレクトリに先ほどコンパイルしたライブラリファイルのあるフォルダを指定します。ここもランタイムライブラリに応じて適切なフォルダを選択します。
- 追加の依存ファイルにSDL.libとSDLmain.libを追加
- プログラム
- SDL解説ページのサンプルとかSDL / OpenGL 分科会を参考に簡単なものを作ってみます。
#include <stdlib.h> #include "SDL/SDL.h" int main(int argc, char *argv[]) { if (SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0 ) { fprintf(stderr, "Unable to init SDL: %s\n", SDL_GetError()); exit(1); } atexit(SDL_Quit); SDL_Surface* screen(SDL_SetVideoMode(640, 480, 16, SDL_SWSURFACE)); if(!screen) { fprintf(stderr,"failed to set video mode to 640x480x16.\n"); return -1; } for(;;) { SDL_Event event; while(SDL_PollEvent(&event)) { if(event.type==SDL_QUIT) return 0; Uint8* keys(SDL_GetKeyState(NULL)); if (keys[SDLK_RETURN]==SDL_PRESSED) return 0; } } return 0; }
- コンパイルを実行。
- コンパイルしたフォルダにSDL.dllをコピーする
- 実行
と、これで簡単なサンプルが完成です。
「まあ、表示してリターンで閉じるだけだけどな」
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-09-27
まだまだ寄り道が続きます。今日もSDLです。
「ン? まだ何かやっているンかい?」
ええ、SDLは良くできたライブラリですが、残念ながら基本はCで実装されています。使い方もCチックです。
「まあ、目的を考えればそうだわな。カリカリにチューンするだろうし、そもそもこの手のライブラリ使うやつがオーバーヘッドの発生しやすいC を使うことは少ないだろうからねぇ」
……いや、いや。最近そこまでストイックなプログラマは少ないと思いますが……。そもそもPCの性能が上っていますし。
まあ、それは置いといて、SDLはCで実装されているということから、
- リソース管理は自分で行う必要がある
- にもかかわらずライブラリが管理しているリソースも存在して、それぞれ扱いを別にしなくてはならない
- 「~~している間は~~してはいけない」といった制限があり、プログラマが気を付けなくてはいけない
- namespaceを使っていないので命名規則がウザったい
という欠点があります。
「……最後のは言い掛かりだねぇ。まあ、RAII信奉者のあンたとしては納得行かないことなンだろうねぇ」
ええ。細かく注意しなくちゃいけないのは面倒です。Cならともかく、C 使っていてこんな細々したことに付き合うのは苦痛以外のなにものでもありません。
ということで、C でラッパーを作ってみることにしました。
「ふうン。で、どういうコンセプト──考え方で行くんかい?」
はい。まずは
- RAII!! RAII!!!! RAII!!!!!! リソースは全部RAIIで管理。SDLの初期化と解放もRAIIにする。
- 基本的にSDLを表すオブジェクトを操作することでSDLの操作を行う。
- サウンドとかグラフとかの一部の機能だけ使う用途にも対応できるように、コンセプトベースで組む。
といった感じですね。じゃ、ちょっとチャレンジしてきます。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-10-27
一ヶ月振りですね。今日は報告だけ。
「何やってたンだい?」
いや、大した話では無いのですが……
- 例外
- 大域脱出(Executorの残りの部分のスキップ)
- Executorを実行(展開)するときに、展開毎に特定のCellを別のCellに変更する機能
- Executorをテンプレート的に使えるように、展開時にIdを入れ替えできるようにした(指定されたIdは仮引数的な挙動をする)。
- Executorを実行(展開)するときに、展開毎に特定のCellをユニークなCell(Id)に変更する機能
- Executorをマクロ的に使う時に、再帰的展開でIdの衝突問題が起らないようにするため、その展開毎にユニークなIdを作れるようにした。
あたりを実装しました。これで実行プロセスに関する部分はほぼ終わりですかね?
「何言ってンのか判らンよ。かなり下回りの処理だっつうのはわかるけど」
……まあ、そのうちドキュメントを整理しますので、とりあえずはご容赦を。
「しかし、Forth Word的な挙動をするExecutorの強化ばっかだねぇ」
ええ、やっぱり突き詰めていくとForth……というか2スタック&命令/辞書という仕組みは素敵ですね。簡素な構造なのに規模が大きくなっても破綻しにくいということで、自己拡張に向いていそうです。
「制約が少なくて何でも受け入れるから、エラーすると酷い目にあうけどな」
その辺も真面目にやらないといけないですよね。とりあえずはライブラリを実装しながら考えますか。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2007-11-04
誰もいませんね……簡単に書き逃げしますか。
式鬼言語の柱の一本になる委譲について。色々と迷いましたがこれで腹を括ることにしました。
- 委譲は深さ優先検索
- なので菱形継承は上手く動作しません
- 基底となるCellがデータを持っている場合、優先順位の低い派生Cellよりも基底Cellが優先されます
- Mix-in的に使用してください
- Cellのデータ(List, Core, Link)は一通り委譲することができるけれども、その後の扱いが違います
- ListとCoreはコピーして取り込む
- 次回からはコピーしたデータを使う
- ListとCoreは加工して使用することを前提としているのでコピーしている
- Linkはそのまま使う
- 次に参照するときも改めて検索する
- 移譲先の参照関係が組み変わっても追随できるように(クラスのカスタマイズを想定しています)
といった感じです。
使い方はこれから研究ですが……とりあえずは空っぽのCellを特定のCellに委譲することでそのCellの挙動をシミュレートできます。まずはクラスのシステムを組みなおしてみますかね。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2008-01-26
メモ。制御構造……というかもっと基本的な実行プロセスについてのアイディア。また迷いが出てきてぐちゃぐちゃです。
「さて、どうするかね。Forth(手続き)よりにするかScheme(継続)よりにするか」
基本的にはForthよりにしたいんですけど、当然構造化プログラム必須ですから継続とも上手く擦り合せしたいところです。色々な機能も暗黙的に持つようにしたいですし。
- 盛り込みたい機能
- 継続
- 手続きからわかるようにする
- 手続きが変更できるようにする
- 継続渡しのサポート
- 手続きから戻り値も渡せる
- 手続きが終了したあとは、次に実行される手続きはその継続
- 構造化プログラムのサポート
- windingとかrewinding、RAIIとかをサポートしたい。
- 実行後に実行するファイナライズスタックをオペレートスタックの他に用意する。
- オペレートスタックが空の時にCellがプロセスに呼ばれたり、Cellをクリアするときにファイナライズスタックの内容を実行する。
- 命令・スタック
- 参照するときは手続きとして実行する
- 遅延実行のForth版
- 実行する手続きと実行しないデータをどうやって見分けるか?
- Popするときに手続きとして実行する??
- 実行プロセスの暗黙化??
- 実装
- 実行プロセスのレベルによって種類を分ける。命名は適当。
- 全部オブジェクト(Cell)
- Monomer:他の手続きの結果(継続)を待たない手続き。(式鬼言語からは)要素的で分割不可能に見える手続き
- 主にC の関数など。
- Fiber:一連のMonomer。属するMonomerの継続を待つ。プロセスは握っておらず、別のFiberに依存している。
- 基本的な動作はPull。
- 他のFiberと通信するための簡単なプロセス間通信機能を持つ。
- FiberからFiberを呼び出した場合はThreadに動作を委譲する??(Threadのオペコードスタック(操作保存用)に押し込む??)
- Thread:一連のMonomer。属するMonomerの継続を待つ。他のプロセスから独立したプロセスを持っている。
- 他のFiberから呼ばれた時は自分自身を戻り値としてFiberに返す。
- まだプロセスを起動していない時は独立したプロセスを作り出す
- プロセスを実行中の時はそのままプロセスを実行
- プロセスも戻り値と一緒に返す。
- 終わったあとは不活性化(二度とプロセスを作り出さない)
- 基本は使い捨て
- 何回も使いたいときはあらかじめCellをコピーしておくか、再度Cellを作り出す。
- Executorは、自分のデータスタックに積まれている手続きを継続のオペコードスタック(操作保存用)にコピーするだけのもの。制御構造ではなく単なる手続き(ForthWord)。
……まだまだ練り込みが甘いなぁ。ちょっと散歩がてらにアイディアを洗ってみます。
…………………………
…………………………
ただいま戻りました。
良く良く考えたら、「Popするときに手続きとして実行する??/実行プロセスの暗黙化??」は無茶ですね。プロセスとは関係の無い普通のpop操作でも実行されてしまうので、制御不能です。止めときましょう。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2008-01-27
ということで、実行プロセス/制御構造について考えていたのですが、…………考えれば考えるほど泥沼ですね。
「そりゃ、色々と機能を追加しようとすればそうだろうな」
まあ、実装言語(C++)側に負担を増やす方向ならそんなに大変じゃないんですけどね……段々そっちの方に腹が決まって来ましたが。
まずは式鬼言語で使用する手続きの分類から。命名は適当です。
- Monomer
- 他の手続きの結果(継続)を待たない手続き。(式鬼言語からは)要素的で分割不可能に見える手続き。
- 遅延評価との関係から、データスタック上のCellを直接扱えない。扱うときは引数取り出しのMonomerで(前もって)処理する必要がある
- C++バインディング
- Polymer
- 手続きの連なり。自分のデータスタックに手続きを保存している。
- 手続き実行時:自分のデータスタックに積まれている手続きを継続のオペコードスタック(操作保存用)にコピーする。
- いわゆるForthWord。
- 以前にExecutorと呼んでいたもの。
- Fiber
- 実行時は属する手続きを逐次実行し、その継続を待つ。
- ローカルな名前空間を持つ。
- プロセスは握っておらず、別のFiberの呼び出しに応じて、必要な分手続きを進める(基本的な動作はPull)
- 他のFiberから呼ばれた時は自分自身を戻り値としてFiberに返す。
- 遅延評価されると、自分の代わりにFiberを実行した結果を返す。
- 他のFiberと通信するための簡単なプロセス間通信機能を持つ。
- Thread
- 他のプロセスから独立したプロセスを作り出す(あるいはあらかじめ持っている)Fiber。
- 実行時は属する手続きを逐次実行し、その継続を待つ。
- 他のFiberから呼ばれた時は自分自身を戻り値としてFiberに返す。
- プロセスを実行中の時はそのままプロセスを実行
- 遅延評価には対応していない(自分を返す)
- 終わったあとは不活性化(二度とプロセスを作り出さない)
- Cloth
- 独立したメモリ空間(GC・Cellホルダー)を持つThread。
- プロセスに属するCellの管理を行う。
- Suit
- プログラムソースを変換するパーザーを持つCloth。
- ソースをコマンド列(Cell)に変換し、その後に処理を行う。
- 通常はプログラム本体
といった感じですかね。
「むむむ、Monomerと遅延評価のところが面倒臭そうだね」
ですね。まあ『参照』というアトミックな(ことを期待される)動作に『遅延評価』が割り込みをかけるのですから当然といえば当然なのですが……。C++がコルーチンに対応していればずいぶん違ったんですけどね。
今日はここまで。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2008-01-28
ということで、昨日の続きを少しだけ。実行プロセスについて。
あいかわらず自分専用メモということで、他の人に読まれることは考えていない酷い説明文になっています。
「昨日は手続きの種類分けだったね」
はい。普通の言語ですとFiber/Thread/Processぐらいの分類だと思いますが、式鬼言語の場合はForth的なものとしてプリミティブ命令的なMonomerとコマンド列のセットPolymerというのを考えています。で、Monomerというのは実装言語のC++で実行される関数(C++バインディング)を想定しているのですが……
「遅延評価のところで困っちまうわけだね」
遅延評価というのは『引数を実際に参照するときにその値を計算する』というもので、これがあると無限に続くストリームとか必要になる都度値を計算するジェネレーターとかを自然に活用できるようになりますので、かなり便利なものです。
ただ、『引数を実際に参照するときにその値を計算する』という遅延評価の挙動は、実装言語のC++と相性が悪いんですよね。
「何で? C++から遅延評価を呼べばいいんじゃないの?」
簡単に実装するならそういう実装でもいいのですが……色々と制限が発生するのと、思わぬ挙動になる可能性があるのが良くないですね。
まず、C++の関数の挙動ですが、基本的には『関数が呼び出されると、中のコードを実行して呼び出し元に戻る』という動作です。コルーチンみたいに動作中に中断することも、Schemeみたいに継続を渡すこともできません。関数は終了するまで抜け出せず、抜け出したあとは元に戻ることもはできません。
「まあ、そりゃそうだ。基本的な構造化プログラムの動作だわな」
で、Monomerは基本的に『C++で実行される関数を想定している』ということで、もしMonomer中で引数を処理したいときはMonomer中から引数を参照する必要があります。
「引数を参照するっつうことは、遅延評価が発生する、つうことだね」
はい、ここで遅延評価が発生します。つまりプリミティブ命令的で分割不可能なはずのMonomerの実行中に別の手続きが実行されるわけです。
「そりゃもうプリミティブじゃないね」
せめて遅延評価が発生した状態でMonomerの実行を中断できればいいのですが、実装言語にC++を使用している限りそんなことはできません。C++のメソッドにもコルーチンがあればなぁ。簡単に中断・復帰ができるのに。
で、具体的にどんな問題が出そうかというと、
- 制御の動作がかなり神経質になる。気を付けないとすぐに破綻する。
- 遅延評価の対象にPolymerを使用するのは不可。Monomerを使うか、あるいはFiber, Thread, Clothで実行して結果を引き出す必要がある。
とりあえず思い付くのはこんな感じですか。他にも色々とありそうですが。
解決方法としては、ざっと
- 遅延評価を捨てる
- Monomerの内部から(遅延評価の発生する)参照は禁止する
- Monomer実行前に、必要な引数の遅延評価を処理しておく
- C++バインディングを実装するときもPolymerの形で実装する
引数を参照するところで分割して、遅延評価を行うMonomerを挿入する - 遅延評価の対象はFiberしか許さない(Fiber以外は遅延評価しない)
- C++バインディングが中断に対応できるようにする。遅延評価前にC++の実行状態を記憶しておき、遅延評価後に状態を復元するようにする(手動でコルーチンをエミュレート。Polymer的な挙動)
といった感じかと。
遅延評価を捨てるのは無しにすると、お手軽に実装するなら3.で、C++バインディングに苦労してもらうなら2-1メインで2-2を併用(ループ処理などで)といったところでしょうか? Monomerの性質が失われますが、4.の案も毛嫌いするほどのものではない気がします。
「どれも悩ましいね」
スマートさからいうと2.なんですけど、実用上は3.でしょうしね。4.は破綻しにくいでしょうけどMonomerの性質が失われるのがちょっと……むむむ、どうしよう?
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2008-02-09
実行プロセスについてちょっと練り込んだのでその報告です。
「どのあたりを見直したんだい?」
基本的には遅延評価対応ですね。遅延評価無しだったら前の定義で問題ないと思いますが、遅延評価を考えるとMonomerの定義を見直す必要がありそうです。遅延評価の処理が中心になります。
- Monomer
- 引数を消費する手続き。
- Monomerだけが(必要に応じて)引数を遅延評価します。
- C++バインディングもMonomer。遅延評価を行う&C++がコルーチンに対応していない関係上、バインディングも特殊な手続きが必要
- 引数を使用するときは、引数を評価するように式鬼言語をセッティングして式鬼言語にプロセスを返す。
- 評価が終わったあとに再度実行を再開する。
といった感じですかね。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2008-02-13
実行プロセスについてまだまだ考え中。
「長いねえ。下手な考え休むに似たりというけどな」
固まってきたから問題ないですよ。例によって自分専用/他の人に読まれることは考えていないメモ書きを散らしておきます。
実行言語がC++ということを想定して、ちょっと考え方を変えました。Suitを完成形≒プログラム全体(複数起動不可)としたのが大きな違いですね。
- Monomer
- 引数を消費する手続き。
- Monomerだけが(必要に応じて)引数を遅延評価します。
- C++バインディングもMonomer。遅延評価を行う&C++がコルーチンに対応していない関係上、バインディングも特殊な手続きが必要
- 引数を使用するときは、引数を評価した後に自分自身を実行するように式鬼言語をセッティングして式鬼言語にプロセスを返す必要がある。
- 評価が終わったあとに手続きを再開する。
- Polymer
- 手続きの連なり。自分のデータスタックに手続きを保存している。
- 手続き実行時:自分のデータスタックに積まれている手続きを継続のオペコードスタックにコピーする。
- いわゆるForthWord。
- Fiber
- ローカルな名前空間を持つ。
- 別の手続きから呼ばれる度に、オペコードスタックに積まれている手続きを順次実行する(プロセスは所有しない)
- Monomerに近い動きだが、中の動作も式鬼言語に管理されているのが大きな違い。
- コルーチン的な動作もOK。手続きの中断も可能。
- 継続のやり取りもOK。
- オペコードスタック自体の操作も可能(条件分岐、継続渡し、繰り返しなどもOK)
- Thread
- 他のプロセスから独立したプロセスを作り出す(あるいはあらかじめ持っている)Fiber。
- 他の手続きから呼ばれた時は自分自身を戻り値として手続きに返し、プロセスを実行する。
- プロセスを実行中の時はそのままプロセスを実行。再起動や中断はしない。
- 終わったあとはそのまま不活性化(再起動はしない)
- Cloth
- 独立したメモリ空間(GC・Cellホルダー)を持つThread。
- プロセスに属するCellの管理を行う。
- Suit
- プログラム本体。
- メインのプロセスを使ってプログラムを実行するCloth。
- プログラム中に1つしか存在できない。
といった感じです。
「Suitの定義はちょっと気に入らないな。前の『ソースコードを解釈する能力がある』というほうが面白いわな」
まあ、Clothに毛が生えたような定義ですからね。“ソースコード≒入力を処理する”という定義の方が本質的なのかもしれませんが……。それらをひっくるめてまとめたもの、という解釈でご容赦を。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2008-02-14
と、いうことで実行プロセスをえっちら実装中。
「といってもコード量は大したこと無いねえ。一時間もあれば書けそうな……」
………………いやいやいや。けっこう大変ですよ?デバックしながらWeb見て、デバックしながらテレビ見て、デバックしながら本読んで…………
「誘惑が多いと大変だね。時間と体力があり余ってる状況において、精神論は圧倒的に正しいということか」
さすがに『「やる」というところまで行かない』ということはありませんが……。世の中には『勝手に楽しませてくれる』ものが多いですからね。誘惑を振り切って実際に手を動かすのは大変です。受け身に慣れてしまうと身体と頭が鈍ってしまいますしね…………。
「で、今日は何だい?」
今日はメソッド呼び出しについて少々。これもアイディアのメモ書きです。まだ色々と問題があるので一度書き出して見ようかと。
「例によって自分専用&閲覧不要かい」
ですね。暗号のように不可解な解説です。自分で言うのも何ですが。
まず、メソッド呼び出しの考え方から。
「基本はCLOSだっけ?」
そうですね。CLOSの多態についてはTiny CLOS 入門に目を通してもらうことにして、基本的な考え方をだらだらと箇条書きで。
- メソッド呼び出し自体も手続きの一つ。
- カリー化の手法で、『データスタックからメソッドを探し出す手続き』を順次データスタックのCellから探して実行する。
- 引数はデータスタックに積まれているCellの一部
- データスタックに積まれたCellを見て該当するメソッドを探す
- メソッドの検索方法は色々(メソッド呼び出しの手続きの実装次第。拡張も可能にする?)
- 総称関数(多態関数)
- データスタックのCellが作り出す木構造の(スーパークラスの)一致(パターンマッチ)(やりたい)
- 条件によるパターンマッチ(条件分岐で代用?)
- 正規表現とかオートマトンとかの受理(できたらいいな)
- パフォーマンスは犠牲にしたくないので、まずは決定性のものに限定(PEGはNG……)
- データスタックのトップから順に検索する
- メソッド呼び出しでは遅延評価の処理は行わない。
- メソッド呼び出しで使用する作業用スタックを用意する
- 作業用スタックなので、他の作業に使われる可能性もあり(他の手続きでCellを積まれることを考慮した挙動にする必要あり)
…………あれ? こんなもんだっけ?
「『正規表現とかオートマトンとかの受理』での多態は無謀の一言だね。そこまで高度にしても誰も使いこなせないだろうに」
まあ、そこは『将来の展望』ということで。実装も大変そうですし。決定性を諦めてPEGにすれば簡単になりそうですけどね。
「まあ、まずはその前に実行プロセスの実装だね」
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2008-08-16
ただいま。長い旅でした。
「もう半年かね。メソッド回りは実装できたんかね?」
もう半年ですね。
グルグルと基本設計含めて見直しまくって、ようやっとメソッド定義含めて足組みを組めました。
まあ、メソッド定義~呼び出しは不満なところがあるのでまだ見直しますが。
…………いや、本当に遠かった。
Cellの基本設計まで踏み込んで見直しましたし、プログラムのフローもずいぶん変わりましたし。コンセプトもちょっと見直したりしたりなんかして。
「今日はその辺のメモかなんかかい?」
いえ、そこは時間がかかるので。もう夜も遅いですし、今日は顔見せだけですね。
半年練っただけはあって、色々と設計は良くなっていますからね。ホントの基礎となる部分のCellの構造とかは確定しても良い……ことにしたいなぁ。
「なんだい、まだ迷っているのかい?」
いえ、…………たぶん大丈夫。まあ、次回はCellの構造についてまとめましょう。
「改善も良いけど、ドキュメントの作成もしないとねぇ。行き詰まって誰かに相談するにしても理解してもらうこともできないからね」
判ってますって。せっかく作るんだったら他人の意見も貰いたいですからね。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2008-08-16
ということで昨日の続きを……というよりも今日の第二回ですね。
「さっそくかね。で、何やるんだい?」
まずは基本的なデータ構造のCellについて簡単に。
式鬼言語では、何でもかんでもCellで構成されています。CellとCellの繋がりでプログラムが構築されていますし、操作の単位もCellが単位になります。コマンドもCell、データもCell。式鬼言語を動かすバーチャルマシンもCell。
「高々一つの構造体でそんなところまで表現できるんかね?」
いえいえ、もちろん実装言語(この場合C++)によるカスタマイズを柔軟に受けられるようにしています。実際にはコアとなる部分の多くがC++での操作ですので、まあ、ある意味誤魔化しですね。さすがにポール・グレアムのArcみたいにプリミティブを追求するところまでは行きません。
「プリミティブを追求するのも魅力的だけどな」
それもそうですが。まあ、それは将来の楽しみにしておきましょう。
まずは実際のCellの構造です。こんな構成になっています。
- Cell
- List: 双頭の列(キュー)
- Link: Cellを検索するマップ
- キーとなるCellから値となるCellを検索するマップ
- 高速にアクセス可能なレジスタ
- Master: 委譲するCellを管理
- Core: 実装言語とのインターフェイス
- コマンドとして実行した時の処理内容
- Cell毎の実装言語のデータ
- 実装言語の単語データ --> Cellの変換テーブル(ArchiveとしてCell全体で共有)
- その他実装言語からCellにアクセスするための各種テーブル(ArchiveとしてCell全体で共有)
「スタックして使用できるListと、辞書として使用できるLinkと……ちょっとしたForthマシンだね、こりゃ」
ええ、Forthマシンを各Cellに押し込んだ感じですね。ただ、通常Forthマシンはデータスタックとリターンスタックの2つを持っていますが、Cellには一つのListしかありません。ですので、式鬼言語のバーチャルマシンはいくつかのCellを組み合せて構成することとしました。最初は各Cellに複数のスタックを押し込んでいたのですが、柔軟性とかカスタマイズ性なんかを考えると各Cellに一つのListの構成の方が良かったんですよ。Cellの構造も簡素になりますし。
バーチャルマシンの話はちょっとややこしいので詳細は後ほど。
「そうすると、Cellはバーチャルマシンを作るのに必要そうな部分に絞り込んで定義した、つうことかね?」
まあ、そんなところですね。バーチャルマシンに必要な構成を考えたら便利そうな機能が一通り揃ったのでそれにした、という所ですか。レジスタはマップで代用可能なのですが、さすがにプログラムの駆動に係わる部分は負荷がかからないようにしたいので、とりあえずはレジスタありとしました。
「あとは……Coreは?」
Coreは……ここが最大の誤魔化しですね。Cell自体は統一された構造で全て同じように扱うことができます。しかし、先ほども言いましたが式鬼言語の場合はコマンドもCellで表現します。何もかも同じだとコマンドの動作も同じになりますので、とてもじゃないですがプログラムなんてできません。
「まあ当然だわな」
ですので、Cellの多様性の部分……汚ない部分も含めて一切合切をCoreの部分に押し込んで固めました。ついでなので実装言語のデータを活用する場合もここに押し込むことにしました。また、式鬼言語のプログラムソースのパースも実装言語側で行いますので、プログラムソース中の単語をCellに変換するのもCoreの一部(ArchiveというCell共通のオブジェクト)の仕事です。
極端な話、CellはCoreを通じて実装言語にプログラムを処理させるためのディスパッチャに過ぎないとも言えますね。
「ふうん。まあ、それを言い始めたら世の中のプログラム言語のほとんどがそうなるから、それはそれでいいんじゃないかね」
そういってもらえると助かりますね。
最後に残ったMasterはちょっと異質で、プロトタイプ指向(インスタンス指向)言語で良く使われる委譲を管理します。これについては別途まとめます。
で、このCellが互いに連携することでプログラムを実行します。次回は式鬼言語の駆動(バーチャルマシン)についてお話しします。
「数ヶ月後とかは無しだよ」
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2008-08-23
では、今日は『駆動』についてです。
「実行プロセスっつうた方が分かり易くないかい?」
何となく実行プロセスというよりも駆動と言った方がしっくり来るんですよね。何でか知りませんが。
さて、前回も軽く触れましたが、式鬼言語は何でもかんでもCellが絡んできます。特に、式鬼言語を動かすバーチャルマシン自体もCellでできているのは特筆すべきことじゃないかと思います。
「普通は別の構造にした方が効率良いからな」
しかし、将来的に式鬼言語からバーチャルマシンを弄ることを考えると、やっぱり同じ構造にしておいた方が便利そうなんですよね。最初はC++ネイティブでバーチャルマシンを構築することも考えましたが、『どうせ誰も使わないお遊び言語なんだからトコトン突き詰めてやってみたほうが面白くね?』ということで突っ走りました。
「そのために一年ぐらい開発が足踏みしたけどな」
あれ、そんなに長かったでしたっけ? まあ、その辺は私の知識不足の部分もありますので。嫌んなって放っといた時期もありますし。
…………ああ、才能と根気が欲しいなぁ。
「で、実行プロセスはどうしたんだい? C++は継続とかコルーチンとか無いからかなり面倒そうだけど」
そうですね。ここいらの実装に関してはC++は不自由ですね。気をつけないとスタックフレーム使い切っちゃいますし。基本的には式鬼言語側で駆動を管理する形にしています。
では、さっそく始めましょう。
まず『駆動(activation)』について。駆動とは、関数とか手続きとかの一連の処理を行うことですね。ぶっちゃけ“実行プロセス”とか“実行ステップ”とか言っても大外れではないと思います。ただ“実行”というとプログラム全体の大きな枠組みを意識する場合がありますが、今回は実行の最小単位となる命令を実行する手順が焦点になりますので、ここでは『駆動』としています。
「『式鬼言語におけるコマンド処理のメインループの中の手順』、つうことかい?」
まあ、そんな感じですね。
他の言語では何と言っているか判りませんが、式鬼言語ではこの駆動が最小の処理単位となります。さらに言うと、駆動一回で処理できないコマンドも多々存在します。
さっそくバーチャルマシンの構造を見ていきましょう。まずは最低限の本当に核の部分だけ。本当はもうちょっと複雑なんですけど、それは核の部分を説明してからにします。式鬼言語のバーチャルマシンでは、YarnとFiberという2種類のCellが中心になります。
- Yarn
- List : [Fiber0, Fiber1, Fiber2, ……, FiberN, CommandX]
- Fiber
- List : [CommandN, ……, Command2, Command1, Command0]
- Map : [<Stack>]
この2つがバーチャルマシンのキモですね。Yarnが現在実行中の駆動を管理して、Fiberが実行中のスコープを管理します。
Yarnの方は簡単ですね。実装言語の駆動をループの形で独占して、ループ一回毎にListのトップにあるCellを実行します。
- 駆動をループとして独占する
- 一回の駆動毎に
- ListのトップにあるCellを取り出す
- Cellに駆動を渡して実行する
- Listが空になったら駆動を手放す(終了)
通常、Fiberのように別のCellを実行するCellは、処理が終了するまで自分自身をYarnのListに積んで実行したCellの終了を待ちます。
「ふうん、なんか関数スタックみたいだね」
そうですね。関数スタックみたいなものです。スタックを活用してスコープの入れ子構造も表現しています。
Fiberの方はもうちょっと面倒ですね。Fiberは呼ばれるごとにListのトップにあるCellを実行します。また、Commandに引数を渡して実行結果を受け入れるために、<Stack>と呼ばれる特殊Cellとリンクしています。基本はスタックマシンなので、<Stack>は一つあれば十分です。
- 別のCellから駆動を渡される
- 一回の駆動毎に
- ListのトップにあるCellを取り出す
- Cellに駆動を渡して実行する
- 駆動を返す
「あれ? さっきのYarnと同じような動きをするね」
はい、そうですね。基本的には同じです。実際には実装言語のC++側の駆動の問題もありますので、FiberのListからYarnのListに積み替えて(実装言語側の関数スタックを抜いてから)Cellを実行していますが……
「Schemeみたいに継続使えると簡単なのにな」
C++の関数スタックはケアしないで無視しようかなぁ。
閑話休題。
FiberとYarnの大きな違いは駆動の占有期間ですね。YarnはList中の全てのCellを処理するまで駆動を手放しませんが、Fiberは1つのCellを処理したら駆動を手放します。
「<Stack>の部分も違うんじゃない?」
ここはちょっと検討中です。Yarnは確かに引数渡し/戻り値受け取りの機能を持たしていませんが、実際にはプログラム全体の引数を渡すのとかプログラムの実行結果を保存するとかに使えるかもしれないので。
「ふうん、YarnのListをコールスタック的に扱って、FiberのListに命令列を保存してアルゴリズムを管理してるのか。Fiber自体には関数フレームの機能もあるから、引数とか戻り値とかはここで管理する、と」
ええ、このようにYarnとFiberの役割を分けることで、YarnとFiberの相似性を上げています。全てのものが斉しい、美しい対称性の世界にようこそ…………
「そこまでは言えないだろ」
まあ、そうですけど……ずいぶん頭悩ませたところなんですから、これぐらいは言わせて下さいよ。
ついでなので、駆動に関係のあるCellを全部挙げちゃいましょう。前に説明した内容から全然別物になってますけど……
- Monomer:
- アトミックな命令を表わすCell
- Polymer:
- 複数の命令を組み合せたCell
- 実行したYarnのトップにあるFiberのListに、実行する命令を流し込む
- Filament:
- 複数の命令を組み合せたCell
- 命令を一つ一つ実行する
- Fiberと異なり、スタックフレームにならない
- FiberのListのトップに自分自身を積む
- Fiber:
- 複数の命令を組み合せたCell
- 命令を一つ一つ実行する
- Yarnのトップに自分自身を積む
- 引数と戻り値を管理する
- Thread:
- 複数の命令を組み合せたCell
- 新しいネイティブスレッドを作成して独占し、命令を全て実行する
- Yarn:
- 複数の命令を組み合せたCell
- 実装言語のプロセスを独占し、命令を全て実行する
といった感じですね。
「呼び名は繊維関係か。だからProcessじゃなくてYarnなのかな?」
そうです。Thread, Fiberの名前から発想を頂きました。まあ、悪くないでしょ?
「どうだろ?混乱しそうな気もするけど」
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2008-08-24
では昨日の続きを。『駆動』の付加的な部分ですね。
「オマケ機能かね?」
そうですね。……といってもけっこう重要な部分ではあるんですが。一つは『前処理・後処理』で、もう一つが『中断』です。
「いわゆるワインディング、アンワインディングとかの話かね?」
そうですね。Lispとかで実装されている機能です。駆動の流れを制御する方法の一つですが、このあたりは言語としてサポートしておいた方が望ましいので付加的要素として追加しています。
始めに『前処理・後処理』について。これはあるCellが実際に処理される前と実行された後に行う処理になります。
「文字通りだね」
そうですね。この機能はFilament、Fiber、Thread、Yarnにあるのですが、この機能を持つCellは下記のような特殊な構造になっています。
- 『前処理・後処理』可能なCell
- Link: [<Enter>, <Leave>]
- List: [実行するコマンド列]
- <Enter>, <Leave>
- List: [『前処理・後処理』で実行するコマンド列]
『前処理・後処理』可能なCellは実行するときに <Enter>、<Leave>とListの中身を確認して、もし実行する中身がある場合はそのCellを実行します。
優先順位は <Enter> >> Listの中身 >> <Leave>となっていますので、結局のところ実行前に<Enter>の中身を実行して、実行後に<Leave>の中身を実行します。
「簡単な話だね」
さて、次は『中断』です。
こっちはちょっとややこしいのですが、『Listのトップに積まれたCellが特別なCellのUndefinedである場合、Listの中身を実行しないで中断処理をする』という処理になります。
「なんか特別なことをしてるのかね?」
そうですね。中断処理はちょっと特別で、
- <Enter> << <Leave>の優先度で、中身を全て実行する。
- <Enter>、<Leave>実行後に
- <Enter>, <Leave>を初期化する(詳細は別途)
- ListのトップのUndefinedを取り去る
という処理を行い、中断後のCellを再開しても問題が発生しないように工夫しています。
中断時・再開時に前処理・後処理を行うことによって、中断したときもリソースを握り続けるということが発生しないようにしているわけですね。
「ふうん。処理の中断というとコルーチンが思い付くけど、これはCellをコルーチン化しても問題にならないように実装を行った、つうことかね?」
そんな感じですね。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2008-08-25
もうちょっとだけ『駆動』の続き。
「まだまだ修正入れてるだろ。大丈夫なのかね?」
オーライ、オーライ。ちょこちょこ調整して何となく良い感じになってきたので、たぶん大丈夫でしょう。
「で、どの辺を調整してるんかね?」
大きなところとしては
- 実装の単純化のために、実装言語側の(暗黙の)駆動をどの程度活用するか?
- 実行効率を上げるために、どの程度の効率化(複雑化)を容認するか?
の二点ですね。
「確かに悩ましいところだね。本来なら構成を簡単にすることを優先したいところだけど、そのせいで実装言語に依存したり、あるいは効率を犠牲にしたりするのは納得いかないしね」
ただ、まだ悩んでいる部分もありますので、今日は簡単にアイディアの説明だけで。
まずはこちらをご覧下さい。CellがCommandとして実行されるときの挙動を指定するCore::executeのC++での宣言です。
void Core::execute(Archive& archive, Cell* machine, Cell* invoker, Cell* command);
命令としてCellを実行するときは、このexecuteを実行して処理を行います。ご覧の通り、この関数には4つの引数が存在します。
「関数を呼び出すとスタックフレームを拵える……スタックフレームには実引数が保存されるから……実行に必要なCellの参照を記憶するにC++側のスタックフレームを活用している、つうわけだな」
そうです。上で言っている『実装言語側の(暗黙の)駆動を活用する』ということですね。archiveは単なる便利変数なので、実際には machine、invoker、commandの3つを記憶するのにC++側スタックフレームを活用しています。
これらのCellはCellを実行するときに重要な役割を果たすCellです。それぞれ下記のような意味を持ちます。
- command: 処理を実行するCell。通常はCore(executeメソッド)を所有するCell。
- invoker: commandを呼び出して実行したCell。<Stack>という引数渡し/戻り値受取用のCellを持つ。通常はYarn、Thread、Fiberのいずれか。
- machine: 式鬼言語側のスタックフレーム(List)を管理するCell。通常はYarnかThread。
特に重要なのがmachineですね。このCellがスタックフレームを管理することで、実行時の効率化とか柔軟性を高めています。
真面目に式鬼言語のスタックフレームを実装するのならば、全てのcommandを実行する前に(machineの)スタックフレームに新しく実行フレームをこしらえて必要な情報を記憶してから実行する必要があるのですが…………そんなことしなくてもC++側のスタックフレームに必要なパーツは揃っていますよね?
ということで、式鬼言語側の実行が破綻しない場合は、式鬼言語側のスタックフレームを更新せずにC++側ので処理を終了するようにしました。
「破綻させないための条件は面倒臭そうだね」
まあ、厳密に考えはじめるとキリがありませんが、まずは妥協点として『commandから別のcommandを呼び出さない。別のcommandを呼び出す時は、式鬼言語側のスタックフレームを更新してから呼び出す』としました。大雑把に言えば、Yarn、Thread、Fiber、ついでにFilamentは他のcommandを呼び出す前にmachineのListを更新しろ、ということですね。
「なんか破綻する条件がありそうな……」
ですね。Yarn、Thread、Fiber(、Filament)を大量に連続的に呼び出すような場合はC++のスタックを食い尽してエラーになります。……まあ、そんな状態は特殊ケースということでまずは忘れましょう。そのあたりまで厳密に対処しようとすると効率がずいぶんと悪化しそうな感じなんですよね。
「『早過ぎる最適化』じゃないのかね?」
かもしれませんが、そんなにややこしくなっていないから大丈夫でしょう。
「中途半端にスタックフレームを分担しているからややこしいのかね。いっそのことC++のスタックフレームに全面依存することにしたら……」
……そうすると、今度は効率がかなり悪化しそうなんですよね。特に再帰的にFiberを呼び出すような状況は最悪です。
これは実行中のCellの構造を見たほうがいいでしょう。
- スタックフレームをC++に依存する
- Yarn List:
- [FiberN List:
- …………
- [Fiber1 List:
- [Fiber0 List:
- ]……N個……]
- YarnのListをスタックフレームとして活用
- Yarn List:
- [FiberN …… Fiber1 Fiber0]
ということで、全てC++に頼った場合、YarnからFiber0を実行するにはFiberN……Fiber1を全て実行する必要があります。YarnのListをスタックフレームとして活用した場合は一発でFiber0を実行することができるのと比べると、ずいぶんとムダですよね。
「むむむ、……そうなると効率もずいぶん違いそうだね」
ええ。そういうことで落とし所として式鬼言語側にもスタックフレームを用意して、C++側のスタックフレームも活用する方法で実装しています。
「ちょっとややこしくなっているけど、……まあ、この程度なら許容範囲か」
まあ、ネイティブコードを考え始めたら結局全部やらなきゃいけなくなりますが、そうなるのはしばらく後になりそうですからね。
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)
2008-09-11
今日は表記方法についてです。『駆動』も色々と弄っているので、こっちもまとめたいところですが……
「まだ手を入れているんかい」
ええ、ちょこちょこ弄っているうちに意外と大規模な変更になってしまって……説明するのも大変ですので今日は簡単な方をちょっぴりだけ。
「表記ってソースコードの記述方法のことだろ?それも大変なんじゃない?」
本当ならそうなんですけど、式鬼言語はForthチックですから、けっこう簡単にできるんですよ。基本はWord実行ですし。
'Hello world!!'.print #=> Hello world!!を表示
とか。
「順番にスタック操作を行って、プログラムを処理するんだっけ?」
ええ、基本的には全部スタック操作です。
ちょっと脱線して中身を説明すると
- 'Hello world!!':スタックにEpigraph(不変文字列)を積む
- .print:スタックに積まれたCellから.printに関連付けられたメソッドを検索して実行する。この場合はEpigraphの内容を標準出力に出力する
といった操作になります。
基本は実行するコマンドを並べていくのですが、一部だけ構造を表現するのに特別な表記方法を使用しています。
それは………今日は語らずにここまでにしましょう。Windowsの自動更新が横槍を入れて来ました。強制終了ですと。
「なんか……ホンのちょっぴりだけだな。ほんと」
制作・著作: 野分(nowake) at fiercewinds.net (Creative Commons 表示-継承 2.1 日本)