数年前、「Flan」というプログラミング言語を作っていました。このプログラミング言語は長い間C++でプログラミングをしてきて感じた不満をもとに、自分好みの最高のプログラミング言語を作ろうと、そういう考えで作っていました。「Flan」は言語機能的にはだいたい完成していたのですが訳*1あって開発は中断していました。
そして中断から数年経ったわけですが、このまま埋もれさせておくのももったいないなと思い、紹介だけでもすることにしました。公開予定は今のところありません。
サンプルコード
百聞は…ということでまずはソースコード例を紹介。機能を詰め込んだサンプルになっているのでちょっとわかりずらいかもしれません。
また、Pythonコードとして無理やり構文カラーを適用してるので一部変な配色になっています。
このサンプルコードが実際に動作するくらいにはプログラミング言語Flanの開発は進んでました。
ここもコメント
-
[int]: a <- 1
[int]: b <- 2print:( a + b ) print:( a - b ) print:( a * b ) print:( a / b )
b = 5
[list<string>]: strList
strList.Append:( 'aaa' )
strList.Append:( 'bbb' )
strList.Append:( 'ccc' )
for i in strList
if i == 'aaa'print:( 'A' )
elif i == 'bbb'print:( 'B' )
elseprint:( 'X' )
end
end
[int]: i
while i <= 3
switch( i )
case 0: assert:( i == 0 )
case 1: assert:( i == 1 )
case 2: assert:( i == 2 )
case 3: assert:( i == 3 )
end
i += 1
end
deffunc:[string]( [int]a, [int]b, [int]c )
[string]a + b + c
end
assert:( func:(123, 456, 789) == '123456789' )
classBasedefdump:() print:( 'Base.dump' )
end
end
classA : [Base] classAInner
end
[float]: m_Value def@init:( [float]value = 0.0
m_Value <- value
end
def@del:()
[stringliteral]: infoStr <- [stringliteral]@typeinfo:() print:( '[' + infoStr + '].@del:()' )
end
defdump:() |override| print:( m_Value )
if false
print:( $this.m_Value )
end
end
defs_func:[int]() |static| 0
end
deffunc:[int]([int]a,[int],[int]c) print:( 'func: ' + a + $arg1 + c )
[self].s_func:()
end
def@unittest:()
end
end
print:( 'staticメンバ関数呼び出し:' + [A].s_func:() )
[A]: a_ins <- 1.23
a_ins.dump:()
[owner:A]: owner_a <- [A].@new:( 4.56 )
owner_a.dump:() deftemplateFunc<T>:([T]param)
assert:( param == 10 )
end
templateFunc<int>:( 10 )
classtestTemplate<T>
[T]: m_ValueT
end
[testTemplate<int>]: tInt
assert:( tInt.m_ValueT == 0 )
trait named_trait
[string]: name <- 'no name'defgetName:[string]()
name
end
defsetName:([string])
name = $arg0
end
end
classCharactor
has named_trait
end
[Charactor]: charactor
charactor.setName:( 'aaa' )
defgetFuncObj:[func0_obj<int>]()
[int]: outerVariable <- 11returndef:[int]() outerVariable += 1; outerVariable end
end
[auto]: funcObj <- getFuncObj:() assert:( funcObj:() == 12 ) assert:( funcObj:() == 13 ) classHoge
[int]: value <- 10
end
[Hoge]: hoge
[fiber]: fiber <- def:()
hoge.value += 1yield
hoge.value += 1yield
hoge.value += 1
end
assert:( hoge.value == 10 )
fiber.resume:() assert:( hoge.value == 11 )
fiber.resume:()
assert:( hoge.value == 12 )
fiber.resume:() assert:( hoge.value == 13 )
fiber.resume:() assert:( hoge.value == 13 )
ここで紹介する以外にも言語機能はたくさんあるのですが、とりあえず大きめなやつ、または個性的なだけ紹介します。
FlanはFlanソースコードをバイトコードへ変換し、それをFlanVM(Flanの仮想マシン)が実行することで動作します。Luaと同じ仕組みです。ただし将来的にはC++ソースコード生成による実行も考えています。
静的型付け
FlanはLuaとは違い静的型付けです。つまり、全ての変数には型があり、型が一致しないと(変換不可だと)コンパイル時点でエラーが発生します。
FlanはC++から影響を受けたオブジェクト指向なプログラミング言語です。C++に存在する、継承やオーバーライドなどの機能はたいてい言語仕様として含まれています。ただし、多重継承はできません。そのかわりにトレイトがあります。
文末にセミコロン不要
FlanはC++とは違い、文末にセミコロンは不要です。セミコロンを使って1行に複数の文を記述することも可能です。
FlanはC++とは違い、コメントアウトを入れ子にすることができます。これはLuaの影響を受けています。
ここもコメント
-入れ子
なコメントアウトも
--可能
-ここはコメントではない
-
前方宣言不要
FlanはC++とは違い、前方宣言が不要です。なぜ不要かというと、構文解析(パース)を2パスで行っているからです*2。
暗黙のreturn
Flanではreturnを明示的に記述しなくても、関数内で最後の式文が自動的にreturn文になります。
deffunc:[int]([int]a,[int]b)
a+b
end
auto型(型推論)あり
Flanでは変数の型としてauto型を使うことができます。これはC++11で導入されたauto型と同じようなもので、初期化の式の型から自動で変数の型を決めることができる機能です。
[auto]: a <- 10
[auto]: b <- 'string'
初期化と代入で違う構文
C++では初期化も代入もどちらも=
で行いますが、Flanでは初期化は<-
、代入は=
で行います。初期化と代入は別の操作なのですから、別の演算子にするべきだと思ったのでこういう仕様にしました。
また、C++では代入は式なのでif文の条件式内で使えたりしましたが、Flanでの代入は文なのでそういうことはできません。
[int]: a <- 10
a = 20
明確な文法
C言語ではこんな書き方ができます。
(int)hoge((1+2)*3);
これをFlanで書くとこうなります。
[int]hoge:((1+2)*3)
C言語では、()
の意味がいくつもあります。上の例では、キャストと関数呼び出しと式の優先順序変更がすべて()
で行われています。Flanではキャストは[]
、関数呼び出しは:()
、式の優先順序変更は()
とすべて区別されています。
なお、Flanでは[]
で囲われた中はすべて型を表します。
変数の中身の交換というのは普遍的な操作なので演算子として存在してもいいのではと思い、交換演算子というものを仕様に入れました。
[int]: a <- 10
[int]: b <- 20
a <=> b
assert:( a == 20 )
assert:( b == 10 )
3種類の参照。所有、共有、弱参照。
Flanには3種類の参照が存在します。1つは所有。これはC++のunique_ptrのようなもので、所有者が1人であることを保証します。この参照が破棄されるとき、参照先も破棄されます。2つめは共有。これはC++のshared_ptrのようなもので、複数の所有者が存在できることを表します。この参照が破棄されるとき、他に所有者がいない場合は破棄されます。3つめは弱参照です。これはC++のweak_ptrのようなもので、所有ではなく"参照"を表します。参照先が破棄されると弱参照はnullとなります。
ソースコードとしては、所有は[owner:Hoge]
、共有は[ref:Hoge]
、弱参照は[wref:Hoge]
という記述方法になります。
すべてが参照ではない
JavaやPythonやLuaなど多くのプログラミング言語ではプリミティブ型以外のすべてが参照なことが多いですが、Flanは値型と参照型が個別に存在します。
[A]: a
[ref:A]: ref_a
Flanの参照型への操作は、基本的にデリファレンスしてから行われます。つまりC言語でいうところの常に*ptr
が行われるということです。例えば参照型変数a
と参照型変数b
があったとして、a = b
とした場合、a
にb
への参照がコピーされるのではなく、a
の参照先にb
の参照先が代入されます。C言語で表すと*a = *b
です。
では参照をコピーしたい場合にはどうしたらいいのか。それを行えるようにするのが参照系演算子です。例えば参照をコピーしたい場合はa := b
とします。=
ではなく:=
を使うのです。
参照系演算子は他にも参照を交換するための:<=>
、参照を比較するための:==
などがあります。どれも通常の演算子の前に:
が付いているのが特徴です。
classA
[int]: m_Value
def@init:( [int]value )
m_Value <- value
end
end
[ref:A]: a0 <- [A].@new:( 0 )
[ref:A]: a1 <- [A].@new:( 1 )
a0 = a1
a1.m_Value = 2assert:( a0.m_Value == 1 )
assert:( a1.m_Value == 2 )
a0 := a1
a0.m_Value = 3assert:( a0.m_Value == 3 )
assert:( a1.m_Value == 3 )
関数オブジェクト(無名関数、クロージャ)
Flanには通常の関数とは別に、値として扱える関数オブジェクトが存在します。この関数オブジェクトは他の言語では無名関数やクロージャとも呼ばれます。Flanの関数オブジェクトはクロージャでもあるので、その関数が定義された環境を保持(キャプチャ)します。
[int]: value <- 11
[auto]: funcObj <- def:[int]() value += 1; value end assert:( funcObj:() == 12 )
assert:( funcObj:() == 13 )
assert:( value == 11 )
引数名の省略(引数名の自動設定)
Flanでは関数の引数名を省略することができます。省略された引数名は$argN
のような名前が自動で設定されます。このときN
には引数の位置が入ります。
deffunc:[int]([int],[int],[int])
$arg0 + $arg1 + $arg2
end
引数リストの展開
Flanでは関数内で$args
を使うことで引数リストを展開することができます。これは受け取った引数をそのまま他の関数に渡す場合などに便利です。
deffunc:[int]([int],[int],[int])
func2:( $args )
end
deffunc2:[int]([int],[int],[int])
$arg0 + $arg1 + $arg2
end
static/非staticで同名のメンバ関数
C++ではstatic/非staticで同じ名前のメンバ関数を作ることができませんでしたが、Flanでは可能です。
classAdeffunc:[stringliteral]()
'func:()'
end
deffunc:[stringliteral]() |static|
'func:() |static|'
end
end
[A]: a
assert:( a.func:() == 'func:()' )
assert:( [A].func:() == 'func:() |static|' )
関数テンプレート
C++のように関数テンプレートが存在します。
deftemplateFunc<T>:([T]param)
assert:( param == 10 )
end
templateFunc<int>:( 10 )
関数テンプレートのテンプレート引数の推論
これもC++にある機能です。関数への引数から、関数テンプレートのテンプレート引数を推論する機能です。
deftemplateFunc<T,T2=bool,T3>:([T]a,[T3]t3)
assert:([T].@typeinfo:() == [int].@typeinfo:() )
assert:([T2].@typeinfo:() == [bool].@typeinfo:() )
assert:([T3].@typeinfo:() == [stringliteral].@typeinfo:() )
end
templateFunc<int,bool,stringliteral>:( 1, '' )
templateFunc:( 1, '' )
流用テンプレート関数
流用テンプレート関数とは、引数リストを他の関数から流用するテンプレート関数です。オーバーロードされた関数群のラッパー関数を作るのに便利です。
defhoge:[stringliteral]([int]a)
'hoge:[stringliteral]([int])'
end
defhoge:[stringliteral]([bool]a)
'hoge:[stringliteral]([bool])'
end
defhoge:[stringliteral]([stringliteral]a)
'hoge:[stringliteral]([stringliteral])'
end
defhoge_wrapper:[stringliteral](<hoge>) print:( 'Pre Hoge' )
hoge:($args)
print:( 'Post Hoge' )
end
hoge_wrapper:( 3 )
hoge_wrapper:( 5, 6 )
内部クラス、内部関数
クラスの内部でクラスを定義することができます。また、関数内部で関数やクラスを定義することも出来ます。
classAclassAInner
end
end
deffunc:()
defInnerFunc:()
end
classInnerClass
end
end
クラステンプレート
C++のようにクラステンプレートが存在します。
classtestTemplate<T>
[T]: m_ValueT
end
[testTemplate<int>]: tInt
assert:( tInt.m_ValueT == 0 )
トレイト
トレイトとはクラスに機能を持たせるための仕組みです。Wikipediaに記事がありますが、言語機能としてトレイトを持つプログラミング言語でも、その意味は微妙に違っているようです。
トレイト - Wikipedia
乱暴に説明すると、実装を持つインターフェイス(Java)です。
trait named_trait
[string]: name <- 'no name'defgetName:[string]()
name
end
defsetName:([string])
name = $arg0
end
end
classCharactor
has named_trait
end
[Charactor]: charactor
charactor.setName:( 'aaa' )
トレイトテンプレート
トレイトもクラスのようにテンプレートが存在します。
ファイバー(コルーチン)
ファイバー - Wikipedia
ファイバーは中断できる関数オブジェクトのようなものです。Luaにおけるコルーチンとほぼ同じものですが、Fiberの方が文字数が短いのと響きがよいのでFlanではFiberという名称にしました。
classHoge
[int]: value <- 10
end
[Hoge]: hoge
[fiber]: fiber <- def:()
hoge.value += 1yield
hoge.value += 1yield
hoge.value += 1
end
assert:( hoge.value == 10 )
fiber.resume:() assert:( hoge.value == 11 )
fiber.resume:()
assert:( hoge.value == 12 )
fiber.resume:() assert:( hoge.value == 13 )
fiber.resume:() assert:( hoge.value == 13 )
クラスに@unittest
というメンバ関数を定義すると、単体テスト用の関数になります(扱いとしてはstaticメンバ関数)。コンパイル時に単体テストフラグが立っていた場合、実行時にすべての@unittest
関数が呼び出されます。
classA
[int]: m_Value <- 10def@unittest:()
[A]: a
assert:( a.m_Value == 10 )
end
end
classB
[int]: m_Value
def@unittest:()
[B]: b
assert:( b.m_Value == 0 )
end
end
FlanIDE
実はプログラミング言語と同時にIDE(統合開発環境)も作っていました。このIDEはQtを使って作りました。下の画像を見ればだいたいわかると思いますが、機能としては以下のようなものを実装しました。一部、実行形式がC++コード生成だったときの名残もあります。
- コードエディタ
- 行数
- 構文カラー
- エラー箇所に下線
- カーソル位置の抽象構文木の表示(ウインドウ下部参照)
- ファイルリスト
- コードモデル(クラスやメンバ一覧)の表示、ソースコードジャンプ
- エラーリスト
- VM(仮想マシン)デバッガー
![FlanIDE]()
![FlanIDE]()
VMデバッガーは最初は実装していなかったのですが、プリントデバッグでVMの動作をデバッグするのがとても辛かったので作りました。世のVM開発者の方々はどうやってデバッグをしているのでしょうか…。
最後にプログラミング言語Flanをどうやって実装したのかを紹介したいと思います。思い出しながら書いているので間違っている部分があるかもしれません…。また、「言語モデル」*3などFlan独特の名称を使ったりしてます。
実行までの流れ
Flanのソースコードから実行までの流れは以下のようになっています。
![Flan実行の流れ]()
パーサー
パーサーはソースコードを受け取り、抽象構文木(AST)を生成します。このパーサーは、パーサージェネレータであるANTLRを使って生成しました。ANTLRはデフォルトではJavaソースコードを生成しますが、C言語コードを生成させることもできます。ちなみに、C言語用のパーサージェネレータは一般的にはlex/yaccが使われるようです。ただし、yaccには抽象構文木の生成機能はありません。
モデルファクトリ
モデルファクトリはパーサーが生成した抽象構文木から、プログラミング言語Flanの言語モデルを構築します。言語モデルとは簡単に言えば、抽象構文木から意味を読み取って新たに構築したデータ構造です。現在の実装ではこの言語モデルはバイトコード生成時だけでなくVM実行時にも必要になります。
バイトコードジェネレータは言語モデルを元に、バイトコードを生成します。
FlanVM
FlanVMはバイトコードを実行するための仮想マシンです。FlanVMにはバイトコードと言語モデルを与える必要があります。このFlanVMによってバイトコードが実行されることでようやくFlanが実行されたことになります。
前の節でFlanがどうやってプログラムを実行しているのかを紹介しました。実行の仕組みだけならこれだけで良いのですが、実際にプログラミング言語を作るとなるとより多くの作業が必要になります。その辺を含めたプログラミング言語を作るためのステップを紹介します。
なお、ここでの説明はFlanの場合のもので、必ずしもこの方法が必要というわけではありません。例えばパーサージェネレータを使わずに自分でパーサーを書くこともできます。
文法を決める
プログラミング言語を作るにはまず文法を決める必要があります。さらに文法を決める前にプログラミング言語にどんな機能を持たせる決めなくてはいけません。ここは楽しい場面ですが、機能を追加すればするほどそれを文法に落とし込むのに苦労することになります。文法は最初にすべてを決めるのではなく少しずつ付け足していくことも可能ですが、新しい文法を導入するとすでに決まっていた文法を修正する必要が出てくる場合があります。実装はあとにしても文法だけは最初から考えておいたほうがいいかもしれません。
文法を厳密に定義する
文法が決まったらそれを厳密に定義します。文法を厳密に定義することはソースコードの構造を決めることでもあります。パーサージェネレータは文法の厳密な定義を必要とします。いきなり、パーサージェネレータ用の文法定義を書いてもいいし、BNF記法で一旦書いてからそれをパーサージェネレータ用の文法定義に落とし込んでもいいでしょう。ANTLRの文法定義方法はBNF記法に近いのでいきなりANTLR用の文法定義を書き始めてもあまり困ることはないです。
パーサーを生成する
パーサージェネレータ用の文法定義ができたらパーサーを生成してもらいます。ANTLRは抽象構文木を生成してくれるのでよいのですが、yaccでは自分で抽象構文木を構築するコードを書かないといけないかもしれません。
言語モデルとはソースコードのデータ構造です。このデータ構造を構築するためのクラス群が必要になってきます。例えばクラス、関数、変数、式、文などを表すクラスです。変数クラス、式クラス、クラスクラスなどを作っていくのはなかなか楽しいかもしれません。言語モデルクラスには各種エラー処理の実装も必要になります。型が一致しない、変数、関数が見つからないなどです。
言語モデルクラスができたら、それらを使って言語モデルを構築する言語モデルファクトリを実装します。言語モデルファクトリはパーサーが出力した抽象構文木を走査して言語モデルを構築していきます。
言語モデルの構築までできるようになったら、あとはそれを実行する仕組みを作るだけです。実行するための仕組みとしてFlanではVMを利用しました。VMとは仮想マシンのことでソフトウェアで実装されたCPUのようなものです。CPUは機械語を読み取って動作しますが、VMはバイトコードを読み取って動作します。
VMを実装するには以下のようなことをします。
- バイトコードのフォーマットを考える
- バイトコード実行の仕組みを作る
- 言語機能の実装に必要な命令セットを考える
- 命令を実装していく
長くなってしまうので詳細は省きます。「バイトコード」「スタックマシン」当たりで検索してみてください…。
VMが出来たので、そのVMが利用するバイトコードを生成するバイトコードジェネレータを作ります。バイトコードジェネレータは言語モデルを走査してバイトコードを生成していきます。
ここまでで「パーサー」、「言語モデルファクトリ」、「バイトコードジェネレータ」が出来ました。これら順番に使うことでソースコードからバイトコードを生成することができます。ただ、このままでは不便なのでこれらの機能をまとめた「コンパイラ」を作りましょう。「コンパイラ」はソースコードを受け取り、「パーサー」、「言語モデルファクトリ」、「バイトコードジェネレータ」を順に使い、バイトコードの生成します。
ソースコードを実行する仕組みを用意する
コンパイラとVMが出来たのであとは、ソースコードを受け取り、コンパイルし、VMにバイトコード(と言語モデル)を渡して実行する仕組みを用意するだけです。
まとめ
プログラミング言語を作るというのは、本当に楽しくて気付いたら1年くらい経過してました。Flanは現在、開発を中断していますがここまで作ったんだからいつか完成まで持っていきたいと思う…ような思わないような。*4