この記事はBrainf*ck Advent Calendar 2019の20日目の記事です。
概要
- Brainf*ckにゼロコスト抽象化*1を入れた言語、CamphorScriptを2014年頃に作った話です
- 「Brainf*ck、コードの再利用さえできれば普通に楽しい言語なのになぁ」という思いを元に、演算子オーバーロードとインライン関数とかを大量導入したラッパー言語です
- Brainf*ckにコードの再利用を足そうというアイデアは https://aike.hatenablog.com/entries/2009/02/09 から得ました
- 「Brainf*ckに新しい要素を付け足した言語をやりたいんじゃなくて、あくまでBrainf*ckを書きたいんだよな」という人のためのコンパイラ・デコンパイラを提供します
- アセンブリを読む際に、理解した部分をCで書いてコンパイル、対象のアセンブリとだいたい一致することを以て理解を確認したりしますよね?それをBrainf*ckでやりたかった
- CやC++に意図的に見た目をかなり寄せているものの、意味論はかなり別物で、Brainf*ck側に寄り添っている
- リポジトリはこちら
自分語り(あんまり本題に関係ないので読み飛ばしてよい)
全ての始まりは、2013年の夏に参加した情報オリンピック夏季セミナーで『すごいHaskellたのしく学ぼう!』を読んだことから始まる。
歴史に残ることとなる
ヌエックの人「ヌエッ」
— きゅうり (@kyuridenamida) 2013年8月28日
などの名言が生み出されているとはつゆ知らず、私はHaskellにドハマリしていったのであった。型システムによって実行前にマズいコードを排除できるというのはなんとすごいことであるかと、当時JavaScriptぐらいしか書いたことがなくCannot read property '0' of undefined
でn時間を溶かした経験がいくらでもあった私は感動して3日ぐらいで本を読み切った覚えがある。ユーザーが自由に演算子を定義できるのも新鮮だったが、その抽象化力を以て言語仕様を小さくし*2、他の言語なら言語仕様としてアドホックに導入するものをライブラリとして提供しているのも面白いと思った。ということで、このHaskellの「高い抽象化により、コア言語の大きさをなるべく削減しつつライブラリに機能を委任」をBrainf*ckに適用し、ゼロコスト抽象だけでCっぽい見た目の言語を作ろう(もちろんHaskellで)として誕生したのがCamphorScriptである。
その後わりと放置していたが、2019年8月頭に人と会ったとき
ピザを食べに行こうと思ったら再帰的関数論とCamphorscriptを布教された(???) #ピザの集い
— 考察に紙とペンを使う (@yosswi414_0) 2019年8月4日
今日は @yosswi414_0 さんにCamphorScriptの説明をしたりした。作ったのが5年前であっても意外と覚えてて熱く語れるもんだなぁと思うなどした。https://t.co/BnPai7Y1MW
— hsjoihs (@hsjoihs) 2019年8月4日
と意外と内容を覚えていて熱く語れることに気づき、せっかくならアドカレに載せようと決めたので書いている。
以下CamphorScriptの紹介
関数と演算子
ということで、
Brainf**k、IOの状態を気にするとかそんな甘ったるいレベルじゃなく、メモリ上の全状態をコード書きが把握しているのを要求してくる。アレつらい。
— Deleting null solves all problems (@Kory__3) 2018年2月13日
を解決するためにメモリの抽象化としての変数を導入するのはもちろん、Brainf*ck中に出てくる頻出パターンを関数や演算子として抽象化できるようにし、しかもそれをライブラリとして提供できるように考えた。特に、Haskellでユーザーが自由に演算子を定義できることに影響され、ライブラリ側でどんどん演算子が追加できるようにした。
Brainf*ckではデータをコピーするにあたっても「Aを破壊し、それと同時にAの値をBとCに移す」という操作のほうが「Aを破壊せずAの値をBに移す」よりも軽い。そのような頻出処理にclear_the_first_and_add_to_the_second_and_to_the_third(a,b,c);
などという名前をつけるのはあまりにも仰々しいので、演算子にしてしまいたい。CamphorScriptでは、標準ライブラリeq_til
をインクルードすることにより、この処理を (b, c) += ~a;
と書くことができる。
【 ~
はC++のデストラクタの†イメージ†で、直後の変数の値がこの文によって破壊され、0になるということを意味する。その破壊される前のa
の値が、+=
の働きによりb
とc
へと足されていく】
…という現象があたかも起こっているかのように書けるようになっている。
とはいえもちろん、当然ながら舞台裏で起こっていることはそのようなことではない。標準ライブラリeq_til
の冒頭は次のようになっている。
#ifndef __STD_LIB_EQ_TIL_DEFINED__ #define __STD_LIB_EQ_TIL_DEFINED__ #include <fixdecl> void (+=~)(char& a;char& z){ while(z){a+=1;z-=1;} } /* a+= ~z */ void (+=~)(char& a,char& b;char& z){ while(z){a+=1;b+=1;z-=1;} } /* (a,b)+= ~z*/ void (+=~)(char& a;char& z * constant char N){ while(z){a+=N;z-=1;} } /* a+= ~z*N */
...
戻り値のところにvoid
と書いてあるが、これは見た目をC/C++に寄せるためのフェイクであり、実情としては関数であることを示すだけの予約語である。当時Rustを知っていたらfn
とかにしていたかも*3
(+=~)
という関数/演算子がオーバーロードされていることが分かるが、今回呼び出される関数はこのうち2番目のものである。void (+=~)(char& a, char& b; char& z)
とあり、なぜか引数区切りに記号にカンマとセミコロンの両方が登場している。これはどういうことかというと、(b, c) += ~a;
は (+=~)(b, c; a);
の糖衣構文であり、関数が演算子として定義されている場合は引数区切りでセミコロンが入るところに演算子を中置して用いることができる、という仕様になっているのだ。
同様に、三番目の (+=~)(char& a;char& z * constant char N)
というのも a += ~z * 8
として呼び出すことを目的として定義されている関数であり、Brainf*ckで頻繁に出てくる掛け算パターンを読みやすくすることができる。
さて、ここまで関数、関数と書いてきたが、もちろん生の関数がBrainf*ckにあるわけはなく、全てインライン展開される。当然ながら再帰(相互再帰含む)は禁止してある。
カスタム構文
制御構文が[]
、C風に言うならwhile
、しかないというのも、Brainf*ckの美しさでありBraif*ckプログラミングのつらさである。ということで構文すらライブラリで定義できるようにした。
syntax if(~ char& a){block;} { while(a) { while(a){a -= 1;} block; } }
と定義することで、if(~a){~~~}
と書いてあるところをwhile(a) { while(a){a -= 1;}~~~ }
と書き換えよ、と宣言することができる。なおblock
は予約語である。もっとマシな構文は思いつかなかったのだろうか*4。
型システム(笑)
関数/演算子の引数にchar&
とかconstant char
とかあるが、一応これに対しては型チェックが入り、 char&
は変数のみを、constant char
は定数のみを受け取る*5。const char
でchar&
とconstant char
の両方が受け取れるようになっている。
「暇があったら是非構造体やら列挙体やらを追加したい」ともう4年ぐらい前から言っているが、その「暇」とやらがあった試しはない。
この型システム(笑)の嬉しさを少しでも上げるべく、「ヌル関数」という言語仕様もある。どういうことかというと、void (+=~)(constant char N;char& z) = 0;
と書いておくことにより、8 += ~a;
といったコードをコンパイルエラーにできるという代物である*6。
プリプロセッサ(笑)
Cのプリプロセッサの劣化版が実装されているのだが、これは既存のCプリプロセッサを使うという発想が無かった(Cプリプロセッサが普通にHaskellに移植されていることを知ったのは開発が結構進んでからである)ために生まれた四角い車輪の再発明であるので、改善していきたいが、実はCプリプロセッサとは仕様が微妙に違い、かつ既存のコードがそれに依存しているため、単純に置き換えるわけにもいかない。
コア言語CompiledCamphorScript
ここでいう「コア言語」はC++における意味(C++の仕様から標準ライブラリを引き算した部分)ではなく、Haskellにおける意味(脱糖して得られるサブセット言語)である。プリプロセッサと関数のインライン展開を済ませ、残っている抽象化は変数ぐらいしか無いという状態である。
具体的なコード例を示すと次の通り。
char a; char b; char c; char dig1; char dig2; char dig3; char g; a+=4; while(a){ b+=4; { while(b){c+=3;b-=1;} } /* c+=12;*/ a-=1; } /* c += 48*/ delete a; delete b; read(dig1); read(dig2); read(dig3); while(c){ dig1-=1; dig2-=1; dig3-=1; c-=1; } { while(dig3){g+=1;dig3-=1;} } while(dig2){ dig3+=5; { while(dig3){g+=2;dig3-=1;} } /* g += 10;*/ dig2-=1; } /* g += ~dig2 * 10;*/ while(dig1){ dig2+=5; while(dig2){ dig3+=5; { while(dig3){g+=4;dig3-=1;} } /* g+=20;*/ dig2-=1; } /*g += 100;*/ dig1-=1; } /* g+= ~dig1 * 100*/ write(g);
こんな記事をここまで読むほどBrainf*ckに慣れ親しんでいる皆さんなら、このコードを手作業でBrainf*ckに直すことなど容易いだろう。もちろん私にとっても容易いのでちゃんとBrainf*ck化するコードは組んであり、それを通すと
++++[>++++[>+++<-]<-]>>>,>,>,<<<[>->->-<<<-]>>>[>+<-]<[>+++++[>++<-]<-]<[>+++++[>+++++[>++++<-]<-]<-]>>>.
という、我々のよく知るBrainf*ckが出てくるわけである。
変数
Brainf*ckではメモリというのはゼロ初期化されているので、使用し終わったメモリの値が0であることが明示できると、それをコンパイラ側で別の変数に使い回すことができるわけだ。ということで、使用済みメモリの値が0であるとプログラマーが保証する文delete
が用意されている。
なお、ブロック内で確保した変数は必ずdeleteしなければならない。RAIIってやつである。関数/演算子がインライン展開される際のブロックにもこの制約は適用されるので、メモリをゴリゴリ食いつぶして返却しない関数が定義できないようになっている。
例えば、変数を変数に非破壊的に+=する処理を書くなら次のようになる(もちろん標準ライブラリにある)(もちろんこれはCompiledCamphorScriptではなくCamphorScriptのコードである)。
void (+=)(char& to; char& from) { char c2 = 0; while(from){ to += 1; c2 += 1; from -= 1;} while(c2){ from += 1; c2 -= 1;} delete c2; }
確保されるメモリの量はコンパイル時に確定する。本当はメモリの配置を上手く工夫することでBrainf*ckに翻訳した際の>と<をなるべく減らせるようにしたかったが、それを実現するためのアルゴリズムもヒューリスティックも当時の私には実装できなかったため、実装されていない。
また、静的解析で全てメモリを握っている都合上、「実行時に長さが決まる0終端文字列を次々なめていく」というようなプログラムはCamphorScriptで書くことができず、そのようなプログラムをBrainf*ckからCamphorScriptへと逆コンパイルしようとするとエラーを吐く。つまり、実はBrainf*ckのサブセットしか現状のCamphorScriptでは表現できない。改善してはみたいが、本質的にCamphorScriptの表現力を爆上げしなければならないということでもあり、なかなか難しそうである。
(「Brainf*ckからCamphorScriptへと逆コンパイル」と書いたが、現状では実際はCompiledCamphorScriptへと逆コンパイルされる。「標準ライブラリと照らし合わせてパターンを検出し自動的に演算子化」とかができたらたのしいのになぁ)
せっかくなので
AtCoder Beginners Selection の PracticeA - Welcome to AtCoder の既存提出 Submission #2237782 をCamphorScript化してみよう。提出されたBrainf*ckソースをそのままデコンパイル…したものは533行あったので、改行とインデントを大胆に手動削除したものがこちら。
char v_0;char v_1;char v_2;char v_3;char v_4;char v_5;char v_6;char v_7; char v_8;char v_9;char v_10; read(v_5); v_5-=10; while(v_5) { v_5-=2; v_6+=6; while(v_6){ v_5-=6; v_6-=1; } while(v_2){ v_1+=1; v_2-=1; } while(v_3){ v_2+=1; v_3-=1; } while(v_4){ v_3+=1; v_4-=1; } while(v_5){ v_4+=1; v_5-=1; } read(v_5); v_5-=10; } read(v_9); v_10+=8; while(v_10){ v_9-=4; v_10-=1; } while(v_9) { v_10+=4; while(v_10){ v_9-=4; v_10-=1; } while(v_6){ v_5+=1; v_6-=1; } while(v_7){ v_6+=1; v_7-=1; } while(v_8){ v_7+=1; v_8-=1; } while(v_9){ v_8+=1; v_9-=1; } read(v_9); v_10+=8; while(v_10){ v_9-=4; v_10-=1; } } while(v_8){ v_4+=1; v_8-=1; } while(v_7){ v_3+=1; v_7-=1; } while(v_6){ v_2+=1; v_6-=1; } while(v_5){ v_1+=1; v_5-=1; } while(v_4){ v_5+=1; v_6+=1; v_4-=1; } while(v_6){ v_4+=1; v_6-=1; } v_6+=10; while(v_5) { v_8+=1; while(v_6){ v_6-=1; v_5-=1; while(v_6){ v_7+=1; v_6-=1; } v_8-=1; } while(v_7){ v_6+=1; v_7-=1; } while(v_8){ v_8-=1; while(v_5){ v_5-=1; } } } v_5+=1; while(v_6){ v_5-=1; while(v_6){ v_6-=1; } } while(v_5){ v_4-=10; v_3+=1; while(v_5) { v_5-=1; } } while(v_3){ v_5+=1; v_6+=1; v_3-=1; } while(v_6){ v_3+=1; v_6-=1; } v_6+=10; while(v_5) { v_8+=1; while(v_6){ v_6-=1; v_5-=1; while(v_6){ v_7+=1; v_6-=1; } v_8-=1; } while(v_7){ v_6+=1; v_7-=1; } while(v_8){ v_8-=1; while(v_5){ v_5-=1; } } } v_5+=1; while(v_6){ v_5-=1; while(v_6){ v_6-=1; } } while(v_5){ v_3-=10; v_2+=1; while(v_5){ v_5-=1; } } while(v_2){ v_5+=1; v_6+=1; v_2-=1; } while(v_6){ v_2+=1; v_6-=1; } v_6+=10; while(v_5) { v_8+=1; while(v_6) { v_6-=1; v_5-=1; while(v_6){ v_7+=1; v_6-=1; } v_8-=1; } while(v_7){ v_6+=1; v_7-=1; } while(v_8){ v_8-=1; while(v_5){ v_5-=1; } } } v_5+=1; while(v_6){ v_5-=1; while(v_6){ v_6-=1; } } while(v_5) { v_2-=10; v_1+=1; while(v_5) { v_5-=1; } } read(v_9); v_9-=10; while(v_9) { v_9-=2; v_10+=6; while(v_10){ v_9-=6; v_10-=1; } while(v_6){ v_5+=1; v_6-=1; } while(v_7){ v_6+=1; v_7-=1; } while(v_8){ v_7+=1; v_8-=1; } while(v_9){ v_8+=1; v_9-=1; } read(v_9); v_9-=10; } while(v_8){ v_4+=1; v_8-=1; } while(v_7){ v_3+=1; v_7-=1; } while(v_6){ v_2+=1; v_6-=1; } while(v_5){ v_1+=1; v_5-=1; } while(v_4) { v_5+=1; v_6+=1; v_4-=1; } while(v_6){ v_4+=1; v_6-=1; } v_6+=10; while(v_5) { v_8+=1; while(v_6) { v_6-=1; v_5-=1; while(v_6){ v_7+=1; v_6-=1; } v_8-=1; } while(v_7){ v_6+=1; v_7-=1; } while(v_8){ v_8-=1; while(v_5){ v_5-=1; } } } v_5+=1; while(v_6){ v_5-=1; while(v_6){ v_6-=1; } } while(v_5) { v_4-=10; v_3+=1; while(v_5) { v_5-=1; } } while(v_3) { v_5+=1; v_6+=1; v_3-=1; } while(v_6){ v_3+=1; v_6-=1; } v_6+=10; while(v_5) { v_8+=1; while(v_6) { v_6-=1; v_5-=1; while(v_6){ v_7+=1; v_6-=1; } v_8-=1; } while(v_7){ v_6+=1; v_7-=1; } while(v_8){ v_8-=1; while(v_5){ v_5-=1; } } } v_5+=1; while(v_6){ v_5-=1; while(v_6){ v_6-=1; } } while(v_5) { v_3-=10; v_2+=1; while(v_5) { v_5-=1; } } while(v_2) { v_5+=1; v_6+=1; v_2-=1; } while(v_6){ v_2+=1; v_6-=1; } v_6+=10; while(v_5) { v_8+=1; while(v_6) { v_6-=1; v_5-=1; while(v_6){ v_7+=1; v_6-=1; } v_8-=1; } while(v_7){ v_6+=1; v_7-=1; } while(v_8){ v_8-=1; while(v_5){ v_5-=1; } } } v_5+=1; while(v_6){ v_5-=1; while(v_6){ v_6-=1; } } while(v_5) { v_2-=10; v_1+=1; while(v_5) { v_5-=1; } } v_5+=1; v_6+=1; while(v_1) { v_6-=1; v_0+=8; while(v_0){ v_1+=6; v_0-=1; } write(v_1); v_5-=1; while(v_1){ v_1-=1; } } while(v_5){ v_5-=1; } while(v_6) { v_7+=1; v_8+=1; v_6-=1; } while(v_8){ v_6+=1; v_8-=1; } v_8+=1; while(v_7) { v_5+=1; while(v_2) { v_6-=1; v_1+=8; while(v_1){ v_2+=6; v_1-=1; } write(v_2); v_5-=1; while(v_2){ v_2-=1; } } while(v_5){ v_5-=1; } while(v_8){ v_8-=1; } while(v_7){ v_7-=1; } } while(v_8) { v_1+=8; while(v_1){ v_2+=6; v_1-=1; } write(v_2); while(v_2){ v_2-=1; } while(v_8){ v_8-=1; } } while(v_6) { v_7+=1; v_8+=1; v_6-=1; } while(v_8){ v_6+=1; v_8-=1; } v_8+=1; while(v_7) { v_5+=1; while(v_3) { v_6-=1; v_2+=8; while(v_2){ v_3+=6; v_2-=1; } write(v_3); v_5-=1; while(v_3){ v_3-=1; } } while(v_5){ v_5-=1; } while(v_8){ v_8-=1; } while(v_7){ v_7-=1; } } while(v_8) { v_2+=8; while(v_2){ v_3+=6; v_2-=1; } write(v_3); while(v_3){ v_3-=1; } while(v_8){ v_8-=1; } } v_3+=8; while(v_3){ v_4+=6; v_3-=1; } write(v_4); v_2+=8; while(v_2){ v_1+=4; v_2-=1; } write(v_1); while(v_1) { read(v_1); write(v_1); v_1-=10; }
うわぁ。これならまだBrainf*ckの方が読みやすいんじゃないか。
と言ってても始まらないので、じっくりコードを読んで繰り返し登場する処理がないかを(3時間半ぐらい掛けて)探すと……
#include <eq_til> #include <stdcalc2> syntax if(~char& a){block} { while(a) { block; while(a){a-=1;} } } syntax if(! ~char& flag1){block} { char a; a += ! ~~flag1; if(~a) { block; } delete a; } infixr 5 (+=!~~); void (+=!~~)(char& a;char& z){ a+=1; if(~z){a-=1;} } infixl -5 (?@,); void (?@,)(char& b; char& a, char& d -= constant char N){ char c; while(b){ b -= N; a -= N; c +=~ b; d -= N; } b +=~ c; delete c; } infixl -5 (?~); /* if d, clear a */ void (?~)(char& d; char& a) { while(d){ d -= 1; clear(a); } } infixr 5 (-|=~~); void (-|=~~)(char& b;char& a){ /* if(a>=b){a=0;b=0;}else{b-=a;a=0;} */ while(a){ char dummy; char d = 1; delete dummy; b ? @, a, d -= 1; d ? ~a; delete d; } } void carry(char& to +<- char& from, constant char N) { char a; a += from; char b; b += N; /* if (from >= N) { b = 0; } else {b = N - from; } */ b -|=~~ a; /*# MEMORY using a #*/ if(! ~b) { from -= N; to += 1; } delete b; delete a; } void read_till_newline(char& buf_1000, char& buf_100, char& buf_10, char& buf_1) { char v_5; read(v_5); v_5 -= '\n'; while(v_5){ v_5 -= 2; v_5 -= 6 * 6; shift_left(buf_1000 +=, buf_100, buf_10, buf_1, ~v_5); read(v_5); v_5-=10; } delete v_5; } void read_till_space(char& buf_1000, char& buf_100, char& buf_10, char& buf_1) { char v_9; read(v_9); v_9 -= 8 * 4; while(v_9){ v_9 -= 4 * 4; shift_left(buf_1000 +=, buf_100, buf_10, buf_1, ~v_9); read(v_9); v_9 -= 8 * 4; } delete v_9; } infixl 0 (,~); infixl 0 (+=,); void shift_left(char& a +=, char& b, char& c, char& d,~ char& e) { a +=~ b; b +=~ c; c +=~ d; d +=~ e; } void baz(char& v_7 ?~ char& v_8, char& a, char& b, char& v_6) { char v_5; if (~v_7) { v_5+=1; if (~a) { v_6-=1; b += 8; a +=~ b * 6; write(a); v_5-=1; } clear(v_5); clear(v_8); } delete v_5; } void bar(char& v_6, char& a, char& b) { char dummy; char v_7; v_7 += v_6; char v_8 =1; delete dummy; baz(v_7 ?~ v_8, a, b, v_6); delete v_7; if(~v_8){ b += 8; a +=~ b * 6; write(a); clear(a); } delete v_8; } char v_0; char ans_1000; char ans_100; char ans_10; char ans_1; read_till_newline(ans_1000, ans_100, ans_10, ans_1); { char buf_1000; char buf_100; char buf_10; char buf_1; read_till_space(buf_1000, buf_100, buf_10, buf_1); ans_1 +=~ buf_1; ans_10 +=~ buf_10; ans_100 +=~ buf_100; ans_1000 +=~ buf_1000; delete buf_1000; delete buf_100; delete buf_10; delete buf_1; } carry(ans_10 +<- ans_1, 10); carry(ans_100 +<- ans_10, 10); carry(ans_1000 +<- ans_100, 10); { char buf_1000; char buf_100; char buf_10; char buf_1; read_till_newline(buf_1000, buf_100, buf_10, buf_1); ans_1 +=~ buf_1; ans_10 +=~ buf_10; ans_100 +=~ buf_100; ans_1000 +=~ buf_1000; delete buf_1000; delete buf_100; delete buf_10; delete buf_1; } carry(ans_10 +<- ans_1, 10); carry(ans_100 +<- ans_10, 10); carry(ans_1000 +<- ans_100, 10); char v_5 = 1; char v_6 = 1; while(ans_1000){ v_6-=1; v_0+=8; ans_1000 +=~ v_0 * 6; write(ans_1000); v_5-=1; clear(ans_1000); } clear(v_5); delete v_5; bar(v_6, ans_100, ans_1000); bar(v_6, ans_10, ans_100); delete ans_1000; delete ans_100; ans_10+=8; ans_1 +=~ ans_10 * 6; delete ans_10; write(ans_1); char v_1; v_1 += 8 * 4; write(v_1); while(v_1){ read(v_1); write(v_1); v_1-=10; }
と、まあちょっとは抽象化できる。
なんか途中に説明していない/*# MEMORY using a #*/
とかいうものがあるが、これはsyntax if(! ~char& flag1){block}
の呼び出し内で宣言されているローカル変数のための領域として、直前のb -|=~~ a;
により実は0クリアされているa
を使ってくれという指令である。こうしないと元の提出コードに一致するBrainf*ckへとコンパイルされない。
今後の課題
上述したこと以外の課題としては、そもそもBrainf*ckでは結局「アドレスを記録しそれを元にメモリにアクセスする」ということが(素朴には)できない(下スライドにあるポインタフラグ付き配列によりわりといい感じにはできるが)以上、抽象化だったりデータ構造を作っていったりというのにも限界がありそうだという、考えてみれば当然の壁がある。といっても、先人たちのBrainf*ckでのデータ構造の実装例についてまだあまり知らないため、調べてみると思いの外抽象化できるパターンなどが見つかるかもしれない。↓は読んだけれども、静的解析で処理できないデータ構造をいい感じにコア言語に組み込み、しかもそこまでアドホックでなくする方法はあるのだろうか。Brainf*ckプログラマの皆様のご意見など賜わることができれば幸いです
*1:当時はそんな単語知らなかった
*2:GHC拡張のマニュアルを日本語訳している私「たしかにHaskell自体の言語仕様は小さいけど、GHC拡張が膨大なんだよなぁ」
*3:当時JavaScriptは知っていたのだからfunctionにする選択肢はあって、それを採用しなかったのは多分無駄に長いからとかだろう。一応戻り値がvoidでない関数を将来実装することを考えていた可能性もあるが
*4:自分で作った言語の批判、一切忌憚なくできるからアドだということに気づいた。
*5:当時は「ロベールのC++」しか読んでいなかったのでconstexprという名前を知らなかった
*6:一見void (+=~)(constant char N;char& z)をただ定義しないだけで済む話にも見えるが、ヌル関数を予め定義しておくとユーザー側で新たにvoid (+=~)(constant char N;char& z)を定義することができなくなるという利点がある。