こんにちは、1ヶ月ぐらいぶりでしょうか。はざまです。ここ最近、オレオレ言語のExpressoの記事を立て続けに上げてきましたが、そもそも言語とはなんなのでしょうか?
ここでいう言語とは、もちろん我々が日常生活で使用する自然言語ではなく、プログラミング言語を指していますが、では、プログラミング言語を作るとはどういうことでしょうか?
プログラミング言語を作るには、大きく分けて4つの工程を理解し、制作する必要があります。レキサー(lexer)、パーサ(parser)、アナライザ(analyzer)、ジェネレータ(code generator)です。アナライザは広く言われている呼称かは知りませんが。
Expressoにおいては、レキサー、パーサはパーサジェネレータを使用して自動生成、アナライザは手動生成、ジェネレータは、式木を活用しています。カタカナのままでは、それぞれの役目のイメージが浮かばないと思うので、和訳すると、順にトークン生成器、構文解析器、構文解釈器、コード生成器みたいな感じになります。順に噛み砕いていきましょう。
まず、トークンとは何かですが、その言語において、最低限の意味をなす文字列を表します。例えば日本語だったら、品詞がトークンに当たりますかね。この文章の頭から日本語のレキサーにかけると、こんにちは,1,ヶ月,ぐらい……などとトークン化された単語が表示されるでしょう。
このトークンを元に、その言語で特定の意味を表す文、式などを構成するのが次のパーサの役目です。同様にこのブログの文章をパースすると、こんにちは、1ヶ月、ぐらい……などとパースされて式(まあ日本語だと、微妙ですが、expressionなら意味が通るでしょう)になり、構文木を生成します。
そして、構成された文、式の意味を解析し、何を意図しているかを解釈するのが、次のアナライザの役目です。このブログの文章で行くと、こんにちは=挨拶、1ヶ月=期間、ぐらい=期間の範囲を指定する……などと解釈されて意味(semantics: 自然言語の分野ではこう呼ばれないと思いますが)が構成されます。
さらに、最後のコード生成のフェーズはその名の通り、解釈してできた動作を実際に行うコンピュータが解釈できるコード(まあ、究極的にはアセンブリですね)に置き換える作業です。日本語の例には例えられないので、例はありません。このようにして、ある言語のソースコードは解釈されて実行できるコードに翻訳されます。上記の工程のうち、Expressoの例でもわかるように、第2ステップまではパーサジェネレータにより、文法規則さえ与えれば、自動化できます。もちろん、手でパーサを書くこともできますが、一般にyaccなどのツールを使う方が、最適化が行われて無駄のない構文解析が行えるでしょう。文法の要素の1つにLL(1)というものがありますが、これは手動で書いたパーサでは担保するのが困難でしょう。もちろん、LL(1)は文法そのものに付随する要素なので、文法自体がLL(1)ならば、手で書こうが、機械で生成しようが、LL(1)になります。
一方、意味解析のフェーズは、自動化が難しい分野です。構文木を本にどう解釈、意味付けを行うかは、言語に依存する部分が大きいためです。同じ=記号でも、ある言語は代入と解釈し、ある言語は等号と解釈するかもしれないのです。同様に、コード生成のフェーズも自動化は困難でしょう。この程度なら自動化する手段はありそうですが、この意味をどのように実行できるコードに反映するかも、実装によって異なってきます。
このように、言語を作るとは、主に4つの工程を通して、ただのテキストを実行できる何かに翻訳するプログラムを構成することです。文法は自然言語のように、テキストおよび記号から合法な文字列を導き出すことであり、一般にLL(1)の文法は、いい文法とされます。LL(1)は、1個先のトークンを読むだけで意味の弁別が行えるという意味だからです。先読みが必要ないということは、それだけ動作も速くなります。
いかがでしたでしょうか?あなたもプログラミング言語を作ってみたくなりましたか?言語を作ることは、主にコマンドラインで動くプログラムを作ることになるので、WebなどGUIありきのプログラミングに慣れている方にはハードルが高いかもしれません。ですが、言語自体ができてきてコードコンプリーションなどのユーザ支援の機能を実装する段階に入ると、GUIのプログラミングも出てきます。もちろん、実用的な言語を作るならば、何かしらの目的がないと続かないでしょうが、お遊び程度にCのサブセットを実装してみるのも一興かもしれませんね^ - ^
主にWeb(JavaScriptやその周辺技術)とNative(ネイティブで動作するプログラミング言語。主にC++)に関するプログラミングメモや他所で公開している自作プログラムの解説、製作日記などを気ままに綴っています。
2018年5月3日木曜日
2018年3月25日日曜日
オレオレ言語、Expressoについて・・・importと外部アセンブリ編
こんにちは、今月何回目かわかりませんが、はざまです。今回もExpressoについてです。
先日の記事で、.NETを標準ライブラリとして使用できるようにしたと書きましたが、今回その仕様をさらに改良して、任意の外部アセンブリを読み込んで相互運用できるようにしました。それに合わせて仕様が曖昧だったimport文も見直したので、簡単に解説しておこうかなと思います。
まず、以下のようなC#のコードがあったとします。
このアセンブリ名がInteroperabilityTest.dllだとして、以下のようなExpressoのコードを記述します。
すると、Console.WriteLineの出力をしつつ、100、[1, 2, 3, 4, 5, ...]、true、[1..11:1]という出力がされます。ご覧の通り、普通FFI(Function Foreign Interface)を使用して呼べるのは関数だけですが、インスタンスの生成、インスタンスメソッドの呼び出しも行えます。ランタイム環境が共通になっている恩恵ですね。また、コンパイル後のILの状態ならば、リフレクションの機能を使用してC#などの他の言語から呼び出すこともできます。まさに「無敵」状態です。.NETを採用した強みが出ていますね。
2018/4/7 追記: 以前からプロパティのgetはできたものの、本日、setも実装し、完全にプロパティを扱えるようになりました。完全な互換性を保持するまであとenumを使えるようにするだけと、あと1歩になりました。2018/4/8 追記: ILのenumも参照できるようになりました。これで完全な互換性を持ったことになるはずです。
C#からC++のコードの呼び出しができたはずなので、C#でラップすることで、C++のライブラリもExpressoから呼べることでしょう。
さて、以前のコードと比べてだいぶ様相が変わっているのが、importの箇所でしょう。以前のコードでは、importの直後は文字列になっていましたが、仕様改訂を経てRustとPythonのimportの合いの子のような見た目になりました。import文をEBNFで記述すると、以下のようになっています。
つまり、Pythonと違ってas節は省略できません。これは、元の識別子そのままでインポートできないための措置です。インポートした識別子には、必ず別名をつけなければなりません。そうでなければ、"::"や"."を含む名前で識別せねばならず、これはExpressoの仕様上できないからです("::"や"."を含む識別子は定義できない)。import節は、.exsファイルからモジュール内の型を指定する場合に、"::"を使います。これはその他の式でも同様です。
一方、C#で生成したDLLを読む場合には、名前空間から型名を指定してください。また、C#などで生成したDLLを読む場合、変数名や関数名をインポートする識別子に指定することはできません。ILコードではアセンブリレベルにフィールドやメソッドを定義できない制約のためです。
from節のファイル名は、記述されているソースファイルに対して相対パスになります。つまり、上のコードでInteroperabilityTest.dllは、このソースファイルと同じディレクトリに存在します。この挙動は、他の.exsファイルを読み込む時も同じです。
こうして、Expressoから自由にC#のコードも呼べるようになったことですし、そろそろセルフホスティングだか、セルフブートストラップだかも視野に入れていきたい・・・
先日の記事で、.NETを標準ライブラリとして使用できるようにしたと書きましたが、今回その仕様をさらに改良して、任意の外部アセンブリを読み込んで相互運用できるようにしました。それに合わせて仕様が曖昧だったimport文も見直したので、簡単に解説しておこうかなと思います。
まず、以下のようなC#のコードがあったとします。
//In TestInterface.cs
using System;
using System.Collections.Generic;
namespace InteroperabilityTest
{
public interface TestInterface
{
void DoSomething();
int GetSomeInt();
List<int> GetIntList();
}
}
// In InteroperabilityTest.cs
using System;
using System.Collections.Generic;
namespace InteroperabilityTest
{
public class InteroperabilityTest : TestInterface
{
public void DoSomething()
{
Console.WriteLine("Hello from 'DoSomething'");
}
public List<int> GetIntList()
{
Console.WriteLine("GetIntList called");
return new List<int>{1, 2, 3, 4, 5};
}
public int GetSomeInt()
{
Console.WriteLine("GetSomeInt called");
return 100;
}
}
}
// In StaticTest.cs
using System;
using Expresso.Runtime.Builtins;
namespace InteroperabilityTest
{
public class StaticTest
{
public static void DoSomething()
{
Console.WriteLine("Hello from StaticTest.DoSomething");
}
public static bool GetSomeBool()
{
Console.WriteLine("GetSomeBool called");
return true;
}
public static ExpressoIntegerSequence GetSomeIntSeq()
{
Console.WriteLine("GetSomeIntSeq called");
return new ExpressoIntegerSequence(1, 10, 1, true);
}
}
}
このアセンブリ名がInteroperabilityTest.dllだとして、以下のようなExpressoのコードを記述します。
module main;
import InteroperabilityTest.{InteroperabilityTest, StaticTest} from "./InteroperabilityTest.dll" as {InteroperabilityTest, StaticTest};
def main()
{
let t = InteroperabilityTest{};
t.DoSomething();
let i = t.GetSomeInt();
let list = t.GetIntList();
StaticTest.DoSomething();
let flag = StaticTest.GetSomeBool();
let seq = StaticTest.GetSomeIntSeq();
println(i, list, flag, seq);
}
すると、Console.WriteLineの出力をしつつ、100、[1, 2, 3, 4, 5, ...]、true、[1..11:1]という出力がされます。ご覧の通り、普通FFI(Function Foreign Interface)を使用して呼べるのは関数だけですが、インスタンスの生成、インスタンスメソッドの呼び出しも行えます。ランタイム環境が共通になっている恩恵ですね。また、コンパイル後のILの状態ならば、リフレクションの機能を使用してC#などの他の言語から呼び出すこともできます。まさに「無敵」状態です。.NETを採用した強みが出ていますね。
2018/4/7 追記: 以前からプロパティのgetはできたものの、本日、setも実装し、完全にプロパティを扱えるようになりました。完全な互換性を保持するまであとenumを使えるようにするだけと、あと1歩になりました。2018/4/8 追記: ILのenumも参照できるようになりました。これで完全な互換性を持ったことになるはずです。
C#からC++のコードの呼び出しができたはずなので、C#でラップすることで、C++のライブラリもExpressoから呼べることでしょう。
さて、以前のコードと比べてだいぶ様相が変わっているのが、importの箇所でしょう。以前のコードでは、importの直後は文字列になっていましたが、仕様改訂を経てRustとPythonのimportの合いの子のような見た目になりました。import文をEBNFで記述すると、以下のようになっています。
"import" ident [ "::" ( ident | '{' ident { ',' ident } '}' ) ] { '.' ( ident | '{' ident { ',' ident } '}' ) [ "from" string_literal ] "as" ( ident | '{' ident { ',' ident } '}') ';'
つまり、Pythonと違ってas節は省略できません。これは、元の識別子そのままでインポートできないための措置です。インポートした識別子には、必ず別名をつけなければなりません。そうでなければ、"::"や"."を含む名前で識別せねばならず、これはExpressoの仕様上できないからです("::"や"."を含む識別子は定義できない)。import節は、.exsファイルからモジュール内の型を指定する場合に、"::"を使います。これはその他の式でも同様です。
一方、C#で生成したDLLを読む場合には、名前空間から型名を指定してください。また、C#などで生成したDLLを読む場合、変数名や関数名をインポートする識別子に指定することはできません。ILコードではアセンブリレベルにフィールドやメソッドを定義できない制約のためです。
from節のファイル名は、記述されているソースファイルに対して相対パスになります。つまり、上のコードでInteroperabilityTest.dllは、このソースファイルと同じディレクトリに存在します。この挙動は、他の.exsファイルを読み込む時も同じです。
こうして、Expressoから自由にC#のコードも呼べるようになったことですし、そろそろセルフホスティングだか、セルフブートストラップだかも視野に入れていきたい・・・
2018年3月16日金曜日
オレオレ言語、Expressoについて・・・intseq編
こんにちは、はざまです。今回も懲りもせずにExpressoについての記事です。
特定の型に関する記事は特に書くつもりなかったのですが、面白い事実に気付いてしまったので、記事にすることと相成りました。
さて、Expressoのintseq型ですが、これは組み込みの型で、Pythonのxrangeや、RustのRange型を思い浮かべていただくと理解が早いかと思います。まあ、有り体に言えば、整数列を生成するgeneratorなので、そこらへんの基本機能は、他の言語と一緒です。Kotlinのrangeはちょっと強くて演算子で包含確認ができるみたいですが、ExpressoはIncludesというメソッド経由です。Expressoでもinはキーワードではあるものの、for文の右辺にしか使えません。
module main;
def main()
{
for let i in 0..10 { // start..end:stepは3項演算子。stepを省略すると1になる
println(i);
}
for let j in 9...0:-1 { //stepを負にすれば、マイナス方向にも行ける。なお、整合性はチェックしてません(2018/4/5 追記: リテラルで指定している場合のみ、整合性をチェックするようになりました。0...9:-1と書くと警告が出ます。変数などを使ってるとチェックされません)
println(j);
}
for let k in 0..10:3 { //もちろん、stepは1以上でもいい
println(k);
}
let a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...]; //将来的には(0..10).select(|x| x);などと書けるようにしたいところ(2018/4/5追記: [0..10, ...];と書けるようになりました)
let a2 = a[2...4];
println(a2);
}
上記のコードで、想定通り[2, 3, 4]が出力されます(まあ、Slice型のToString()の結果なので、[List<int>, 2...4:1]みたいな感じですが)。a2の型はこちらも組み込みのslice型になります。Slice型自体は、Rustにもあるものの、このような書き方ができる言語がないので(修正: Rustでもできましたね、この書き方)、特筆すべきところはそこになります。
Kotlinだと、floatに対してもrangeが定義されてるみたいですが(floatの数列って意味をなさない気がするから気のせいなのかな)、Expressoの場合は、intだけです。その範囲外は多分死にます(型しか見てないので、範囲外が来ても明示的なエラーにできず、intへの型変換で落ちるはず)(2018/4/5追記: これは誤りでした。実際にはintの範囲外はdoubleと解釈され、intseqはdoubleを受け付けないと怒られます)。
ちなみにrange operatorは、Expressoに2種類ある3項演算子の1つです。もう一つは、有名な条件演算子( ? : )です。
2018/4/5追記: aの宣言のコメントにあるように、intseqでシーケンスを初期化できるようになりました。意外にも、RustやKotlin、Swiftなどではこの表記は採用されていないので、私の観測範囲ではExpresso固有の機能になります。なお、[0..10];と書けば、arrayにもできます。もちろん、stepを指定して、[0..10:2];などと書くこともできます。機能限定のリスト内包表記みたいな感じですね。
2018年3月15日木曜日
オレオレ言語、Expressoについて・・・関数編
こんにちは、はざまです。今回も例に漏れず、Expressoについての記事です。今回はちょっとだけ具体的な文法に触れます。
どんなプログラミング言語にも当たり前にある要素の1つとして、関数が挙げられます。もちろん、Expressoにも存在しますが、Expressoの場合、モジュールの関数と、クラスのメソッドが存在します。実装上は、前者はモジュールクラスのstaticメソッド、後者は普通にクラスのメソッドです。
このコードもテストコードから引っ張ってきたものですが、Expressoの関数は、戻り値は常に省略しても、本体から推論されますが、この仕様を変えるか悩んでいます(上記の例は、全てintを返す関数と解釈されます)。Kotlinでは、Unit(Voidとほぼ同義)の時のみ推論が効くので、Expressoもそうすべきかもと思っています。
引数の方は、デフォルト値がある時のみ推論が効きます(上記の例には存在しませんね)。
上記の例は上から解説すると、
余談ですが、可変長引数をとる関数のパースはできるようになっているものの、コード生成をするようにしてないので、まだ動きません。とりあえず、print*系のために導入したもので、C#にもあるからまあいいかと思っているものですが、具体的なユースケースがそれほど思いつかず、どうしようか悩んでいます。実装自体はそんなに難しくないので、とりあえず入れてしまうのが正解なのかな〜・・・同様に、一般にString interpolationと呼ばれる機能も導入するか迷っています。これがあると、printFormat関数を置き換えることができるんですよね。実に悩ましいところです。(2018/4/8 追記: string interpolationを実装し、printFormat関数を廃止しました)
どんなプログラミング言語にも当たり前にある要素の1つとして、関数が挙げられます。もちろん、Expressoにも存在しますが、Expressoの場合、モジュールの関数と、クラスのメソッドが存在します。実装上は、前者はモジュールクラスのstaticメソッド、後者は普通にクラスのメソッドです。
module main;
def test()
{
let a = 10;
return a + 10;
}
def test2(n (- int)
{
return n + 10;
}
def test3(n (- int) -> int
{
return n + 20;
}
def test4(n (- int) -> int
{
if n >= 100 {
return n;
}else{
return test4(n + 10);
}
}
def main()
{
let a = test();
let b = test2(20);
let c = test3(20);
let d = test4(80);
println(a, b, c, d);
}
このコードもテストコードから引っ張ってきたものですが、Expressoの関数は、戻り値は常に省略しても、本体から推論されますが、この仕様を変えるか悩んでいます(上記の例は、全てintを返す関数と解釈されます)。Kotlinでは、Unit(Voidとほぼ同義)の時のみ推論が効くので、Expressoもそうすべきかもと思っています。
引数の方は、デフォルト値がある時のみ推論が効きます(上記の例には存在しませんね)。
上記の例は上から解説すると、
- 引数なし、戻り値省略
- 引数あり、戻り値省略
- 引数あり、戻り値明示
- 引数あり、戻り値明示の再帰呼び出し
余談ですが、可変長引数をとる関数のパースはできるようになっているものの、コード生成をするようにしてないので、まだ動きません。とりあえず、print*系のために導入したもので、C#にもあるからまあいいかと思っているものですが、具体的なユースケースがそれほど思いつかず、どうしようか悩んでいます。実装自体はそんなに難しくないので、とりあえず入れてしまうのが正解なのかな〜・・・同様に、一般にString interpolationと呼ばれる機能も導入するか迷っています。これがあると、printFormat関数を置き換えることができるんですよね。実に悩ましいところです。(2018/4/8 追記: string interpolationを実装し、printFormat関数を廃止しました)
2018年3月9日金曜日
オレオレ言語、Expressoについて・・・.NET Frameworkを標準ライブラリとして扱う編
こんにちは、はざまです。今回もExpressoに関する記事です。
今まではSystem.Mathなどの限られたクラスしか使えていませんでしたが、つい先日の更新でC#の型のnewとプロパティの読み出し、メソッド、コンストラクタのオーバーロード解決ができるようにしました。これで実質、.NET Framework全体をExpressoの標準ライブラリとみなすことができ、大きな財産となるはずです。また、.NET Frameworkの相互運用のためにnullリテラルを導入しました。nullリテラルは、.NET関連の文脈以外では禁止になる予定です。Expressoの文脈で値の有無を表現するには、別途用意するOption型を使用することにする予定です(Expresso独自の標準ライブラリをどう提供するかは未定です)。
テストのコードをまるまんま写してきただけですが、このようにして.NETの型を活用できます。コンストラクタの呼び出しは、オブジェクト生成式にコンストラクタの引数の型の値を順番通り指定することで呼び出します。コンストラクタ・メソッドのオーバーロード解決は型のみを見て行っており、名前は今のところ見ていないのでなんでも構わないのですが、将来的には何かそこもバリデーションをかけたいところではあります(上記の例では、コンストラクタの引数名をオブジェクト生成式のフィールド名にしています)。なお、オブジェクト生成式でExpressoのclassを生成する場合には、フィールドが定義された順にフィールドの初期値を設定します。フィールド名をオブジェクト生成式で指定しても、その通りには現状解釈されないので注意してください。適当な初期値を与えたい場合は、ファクトリ関数を作成してください。ただし、クラス内に自身を返したり、自身を引数にするメソッドは書けないので注意です(なので、ファクトリ関数をmoduleの関数にするといいでしょう)。
C#のusingのような構文がないので、このようなtry, finallyの不恰好なコードになっていますが、これも新しい構文を追加するべきでしょうね、きっと。あと、予定としては、immutableな変数からは、自身をmutateするメソッドは呼べないようにも変更します(このコードだと、writer.writeが該当)(2018/4/8 追記: この日現在、実装されていますが、外部アセンブリ由来のコードに対してはチェックしていないので、エラーにはなりません。)。あと、現状Enumがないので、.NETのEnumを使用するコードも書けません。ExpressoのEnumは、Rustのような数学的データ型にする予定なので、導入したとしても、.NETのEnumは呼べない公算が大きいですが、ファイル生成の全パターンを網羅できなくなるので、なんとかします(多分)(2018/4/8 追記: .NETのenumを参照できるようになりました。これと別にExpressoのenumは数学的データ型として実装する予定です)。最後に、.NETのメソッドは、Expressoに合わせてキャメルケースに変換してあります。上のコードをFile.OpenWriteなどと書いて、あれ動かねえなんて茶番を演じないように気をつけましょう。(2018/4/8 追記: この仕様は複雑になるので、廃止しました)
今まではSystem.Mathなどの限られたクラスしか使えていませんでしたが、つい先日の更新でC#の型のnewとプロパティの読み出し、メソッド、コンストラクタのオーバーロード解決ができるようにしました。これで実質、.NET Framework全体をExpressoの標準ライブラリとみなすことができ、大きな財産となるはずです。また、.NET Frameworkの相互運用のためにnullリテラルを導入しました。nullリテラルは、.NET関連の文脈以外では禁止になる予定です。Expressoの文脈で値の有無を表現するには、別途用意するOption型を使用することにする予定です(Expresso独自の標準ライブラリをどう提供するかは未定です)。
module main;
import "System.IO.File" as File;
import "System.IO.FileStream" as FileStream;
import "System.Text.UTF8Encoding" as UTF8Encoding;
def main()
{
var writer (- FileStream;
try{
writer = File.OpenWrite("./some_text.txt");
let bytes = UTF8Encoding{encoderShouldEmitUTF8Identifier: true}.GetBytes("This is to test writing a file");
writer.Write(bytes, 0, bytes.Length);
}
finally{
if writer != null {
writer.Dispose();
}
}
}
テストのコードをまるまんま写してきただけですが、このようにして.NETの型を活用できます。コンストラクタの呼び出しは、オブジェクト生成式にコンストラクタの引数の型の値を順番通り指定することで呼び出します。コンストラクタ・メソッドのオーバーロード解決は型のみを見て行っており、名前は今のところ見ていないのでなんでも構わないのですが、将来的には何かそこもバリデーションをかけたいところではあります(上記の例では、コンストラクタの引数名をオブジェクト生成式のフィールド名にしています)。なお、オブジェクト生成式でExpressoのclassを生成する場合には、フィールドが定義された順にフィールドの初期値を設定します。フィールド名をオブジェクト生成式で指定しても、その通りには現状解釈されないので注意してください。適当な初期値を与えたい場合は、ファクトリ関数を作成してください。ただし、クラス内に自身を返したり、自身を引数にするメソッドは書けないので注意です(なので、ファクトリ関数をmoduleの関数にするといいでしょう)。
C#のusingのような構文がないので、このようなtry, finallyの不恰好なコードになっていますが、これも新しい構文を追加するべきでしょうね、きっと。あと、予定としては、immutableな変数からは、自身をmutateするメソッドは呼べないようにも変更します(このコードだと、writer.writeが該当)(2018/4/8 追記: この日現在、実装されていますが、外部アセンブリ由来のコードに対してはチェックしていないので、エラーにはなりません。)。あと、現状Enumがないので、.NETのEnumを使用するコードも書けません。ExpressoのEnumは、Rustのような数学的データ型にする予定なので、導入したとしても、.NETのEnumは呼べない公算が大きいですが、ファイル生成の全パターンを網羅できなくなるので、なんとかします(多分)(2018/4/8 追記: .NETのenumを参照できるようになりました。これと別にExpressoのenumは数学的データ型として実装する予定です)。最後に、.NETのメソッドは、Expressoに合わせてキャメルケースに変換してあります。上のコードをFile.OpenWriteなどと書いて、あれ動かねえなんて茶番を演じないように気をつけましょう。(2018/4/8 追記: この仕様は複雑になるので、廃止しました)
2018年2月17日土曜日
オレオレ言語、Expressoについて・・・骨格解説編
こんにちは、はざまです。今回も前回に引き続き、自作言語Expressoの解説をしていこうと思います。
以下、まずはいくつか特筆すべきと思われるExpressoの機能を紹介します。
まず、Expressoでは、組み込み型でvectorやdictionaryがサポートされています。これらを生成するリテラルが用意されていますし、多少コンパイルでも特別扱い(実体は、それぞれSystem.Collections.Generic.ListとSystem.Collections.Generic.Dictionaryですが)されます。ただ、今の所は、実装上の問題により、パターンマッチの対象にはなっていないのですが……
他に組み込み型関連では、intseqという型があります。intseqとは"integer sequence"の略で、これはPythonで言うところのxrange型やRustのRange型と同様、整数列を生成するジェネレータです。残念ながら、int(32ビットの整数)の範囲の整数しか扱えないのですが、いわゆるCのような旧式のfor文が存在しないExpressoにおいて、カウントアップなどを行う際に多用される型です。vectorやarrayなどに作用して、整数列にマッチする要素だけを取り出すiterator(.NET用語だと、enumerator)も生成できます(sliceと呼ばれる)。
Expressoには、Rustにも存在するmatch文があります。これは、最近はやりのパターンマッチを行う文法要素で、各種オブジェクトの分解や、リテラル値とのマッチングを行います。tupleパターンとのマッチ程度しか想定していませんが、一応変数宣言(let文var文)でも、パターンが使用できるようになりました。
一つ書き忘れてましたが、クロージャは、関数やメソッドに直接渡すなど、すぐにその引数の型を推論できる状態であれば、型を省略することができます。将来的にEnumerable拡張のメソッドを呼べるようになった際にメソッドチェーンを書きやすくするための実装ですが、拡張メソッドを導入するかどうか悩んでいます。型を定義する度に、全型を走査しなければならなくなりそうで、ちょっと微妙なんですよね・・・
いくつか特筆に値する機能を見たところで、皆様も気になっているかもしれないExpressoの原理について解説しましょう。まず、現状、コンパイラは純C#製です。レキサ、パーサー、アナライザ、コード生成、全部C#で完結しています。吐かれるバイナリは、C#のコンパイル後の表現であるIL形式ですし、パーサージェネレータもC#のものを使用しています。この部分もC#を選んだ理由の一つに挙げられるでしょう(式木と呼ばれるデータ構造を生成するだけで実行可能なコードが生成できる)。いずれ、セルフホスティングしてコンパイラ自体をExpressoで実装したいところなのですが、パーサーとアナライザの切り離しをどうするか、パーサーはC#のものを使用するとして、現状、パーサーとアナライザは三位一体なので、そうするとあとはコード生成部分程度しかExpressoで書ける部分がなくなり、結局今のままと大して変わらないのではないかなどの問題があり、まだ実現していません。また、Expresso化するにあたって、組み込みのオブジェクト(intseq,slice)の実装をどうするかという問題もあります。intseq型は、ExpressoIntegerSequenceという型をC#で定義しているのですが、C#の機能を利用してEnumeratorを生成しやすくしているので、Expressoに置き換えるなら、それを自分で実装しなくてはならなくなります(コンパイラが自動で行う変換なので、それを知っていれば大した問題ではないかもしれません。yield式を使用するので、状態を保持するステートマシンみたいなものを自分で書かなければならない)。
とまあ、問題はあるものの、コード生成が楽だったり、パーサージェネレータが存在したり、標準でクロスプラットフォームで動くので、C#はオレオレ言語作りに結構向いている環境と言えるかもしれません。まあ、今から言語作りをしたい方にはいきなり言語作りするのではなく、まずはLISPのインタープリタあたりを実装するところから始められることをお勧めしますが。上で出したレキサ、パーサー、インタープリタ、それぞれの機能を具体的にイメージできるようになります。
次に文法についてですが、現状明文化していないので、Cocoのパーサー定義を見ていただくのが一番早いかと思います。中には、パーサー定義を作ったものの、機能の実装をしていなくて動かないものもありますが(具体的にいうとcomprehension,interfaceなどです)。大雑把に文法を把握したいのなら、ExpressoTest/sources配下のファイルが参考になるでしょう。こちらも仮で書いただけの定義があったりして、パースもできないものが含まれていたりしますが、概要を知りたいだけなら十分と思われます。
ドキュメントについては、Rustの公式解説本のようなものを英語でmarkdown形式で書いています。日本語で書き直すのは面倒なので、しないかもしれません。こちらは、Expresso/Documentation/配下に存在します。
まだ、書きたいことはあるような気がしますが、今回の記事はこの程度で、どうしても書いておきたいことができたら、また記事にしようと思います。では( ̄^ ̄)ゞ
以下、まずはいくつか特筆すべきと思われるExpressoの機能を紹介します。
まず、Expressoでは、組み込み型でvectorやdictionaryがサポートされています。これらを生成するリテラルが用意されていますし、多少コンパイルでも特別扱い(実体は、それぞれSystem.Collections.Generic.ListとSystem.Collections.Generic.Dictionaryですが)されます。ただ、今の所は、実装上の問題により、パターンマッチの対象にはなっていないのですが……
他に組み込み型関連では、intseqという型があります。intseqとは"integer sequence"の略で、これはPythonで言うところのxrange型やRustのRange型と同様、整数列を生成するジェネレータです。残念ながら、int(32ビットの整数)の範囲の整数しか扱えないのですが、いわゆるCのような旧式のfor文が存在しないExpressoにおいて、カウントアップなどを行う際に多用される型です。vectorやarrayなどに作用して、整数列にマッチする要素だけを取り出すiterator(.NET用語だと、enumerator)も生成できます(sliceと呼ばれる)。
Expressoには、Rustにも存在するmatch文があります。これは、最近はやりのパターンマッチを行う文法要素で、各種オブジェクトの分解や、リテラル値とのマッチングを行います。tupleパターンとのマッチ程度しか想定していませんが、一応変数宣言(let文var文)でも、パターンが使用できるようになりました。
一つ書き忘れてましたが、クロージャは、関数やメソッドに直接渡すなど、すぐにその引数の型を推論できる状態であれば、型を省略することができます。将来的にEnumerable拡張のメソッドを呼べるようになった際にメソッドチェーンを書きやすくするための実装ですが、拡張メソッドを導入するかどうか悩んでいます。型を定義する度に、全型を走査しなければならなくなりそうで、ちょっと微妙なんですよね・・・
いくつか特筆に値する機能を見たところで、皆様も気になっているかもしれないExpressoの原理について解説しましょう。まず、現状、コンパイラは純C#製です。レキサ、パーサー、アナライザ、コード生成、全部C#で完結しています。吐かれるバイナリは、C#のコンパイル後の表現であるIL形式ですし、パーサージェネレータもC#のものを使用しています。この部分もC#を選んだ理由の一つに挙げられるでしょう(式木と呼ばれるデータ構造を生成するだけで実行可能なコードが生成できる)。いずれ、セルフホスティングしてコンパイラ自体をExpressoで実装したいところなのですが、パーサーとアナライザの切り離しをどうするか、パーサーはC#のものを使用するとして、現状、パーサーとアナライザは三位一体なので、そうするとあとはコード生成部分程度しかExpressoで書ける部分がなくなり、結局今のままと大して変わらないのではないかなどの問題があり、まだ実現していません。また、Expresso化するにあたって、組み込みのオブジェクト(intseq,slice)の実装をどうするかという問題もあります。intseq型は、ExpressoIntegerSequenceという型をC#で定義しているのですが、C#の機能を利用してEnumeratorを生成しやすくしているので、Expressoに置き換えるなら、それを自分で実装しなくてはならなくなります(コンパイラが自動で行う変換なので、それを知っていれば大した問題ではないかもしれません。yield式を使用するので、状態を保持するステートマシンみたいなものを自分で書かなければならない)。
とまあ、問題はあるものの、コード生成が楽だったり、パーサージェネレータが存在したり、標準でクロスプラットフォームで動くので、C#はオレオレ言語作りに結構向いている環境と言えるかもしれません。まあ、今から言語作りをしたい方にはいきなり言語作りするのではなく、まずはLISPのインタープリタあたりを実装するところから始められることをお勧めしますが。上で出したレキサ、パーサー、インタープリタ、それぞれの機能を具体的にイメージできるようになります。
次に文法についてですが、現状明文化していないので、Cocoのパーサー定義を見ていただくのが一番早いかと思います。中には、パーサー定義を作ったものの、機能の実装をしていなくて動かないものもありますが(具体的にいうとcomprehension,
ドキュメントについては、Rustの公式解説本のようなものを英語でmarkdown形式で書いています。日本語で書き直すのは面倒なので、しないかもしれません。こちらは、Expresso/Documentation/配下に存在します。
まだ、書きたいことはあるような気がしますが、今回の記事はこの程度で、どうしても書いておきたいことができたら、また記事にしようと思います。では( ̄^ ̄)ゞ
2018年2月15日木曜日
オレオレ言語、Expressoについて・・・導入編
こちらでは、ご無沙汰しています、はざまです。
突然ですが、プログラマをされている皆様が生涯で何としても作り上げたいプログラムはなんでしょうか?これは私の願望も多分に含まれているのですが、恐らく一定数の方が、自作言語と答えるのではないでしょうか?
というわけで今回の記事は、自作言語のExpresso(エクスプレッソ)の紹介をします。4,5年前から開発しているオレオレ言語なんですが、最近、try,catchなども実装し、それなりに使える言語になってきたので、α版として公開することにしようかと思った次第です。とは言っても、専用のサイトはまだ用意しませんが。
Expressoという名前は、ExpressiveとEspressoからの造語です。表現力豊かに、かつエスプレッソ(コーヒー)一杯飲む間にでも開発できるような簡潔な言語を目指すという意味を込めています。また、言語のスローガンとして"Easy for beginners, elegant for enthusiasts"という標語も掲げています。「初学者には簡単に、熱狂者には華麗に」という意味ですね。この標語にはあえて、eで始まる単語を多用しています。これは「e(いい)を探す言語」というダジャレです。
Expressoは、オレオレ言語でありながら、普及、実用化させるならPascalのような教育用言語の地位を目指しています。そのために、先ほどの標語のような目標を掲げているわけです。つまり、初学者には簡単に書ける言語、しかしながら、習熟者にとっても、書きやすい言語を目指すということです。
なぜ、教育用言語を開発するのか不思議に思っているの方もいるかもしれないので、解説しておくと、私は大学時代にPascalという言語でプログラミングを学んだのですが、Wikipediaには教育用と書いてあるんですよ。200x年にしては時代錯誤的な構文とまだ設計に慣れていなかったせいもあって無事単位を落とし、翌年Cに変わったカリキュラムで再履したんですが、その現状を変えたいと思ったのがきっかけです。まあ、スピードとかを売りにしてもRustに敵うわけがないという本音もあります。
言語仕様としては、まだα版と銘打ってることもあって、かなりガバガバなところが多いんですが、基本は静的型付け、オブジェクト指向を基本とするマルチパラダイム言語になっています。一番強く影響を受けている言語が、Rustなので、Rustで採用されている仕様が結構入っています。
現状、Expressoは、.NET環境上でのみ動く言語になっています。理由は、.NETだと比較的ランタイム環境を整えるのも楽ですし、クロスプラットフォームで動くのが大きいです。まあ、私が、いちばん好きな言語がC#だからというのもありますが……
伝統的なHello worldプログラムの解説をする前に、導入方法を紹介しましょう。現状、専用のサイトがないので、Githubのリポジトリからcloneして導入していただく形になります。こちらからcloneしてください。cloneしたら、git submodule update --initを実行してください。依存リポジトリの解決が行われます(といっても、一つしかありませんが)。そして、cloneしたディレクトリ/ExpressoTest/配下にtest_executablesというディレクトリを作成してください。ここにテストで使用するバイナリが吐かれる設定になっているので、これがないとテストが実行できません。gitにこのディレクトリを追加できるのなら、追加したいところではあります。
2018/4/7 追記: それからソリューションをVisual Studioなどで開いて、InteroperabilityTestプロジェクトをビルドし、出力されたDLLファイルを/ExpressoTest/sources/for_unit_testsディレクトリに移動してください。
そうしたら、Mac,Linuxユーザの方なら、あとはメインのソリューションファイルをIDEで開けば、ビルドして実行できるはずです(NuGetを使って一部の依存プロジェクトをダウンロードするようになったので、自動で手元にないプロジェクトをダウンロードするよう、IDEの設定を変える必要があるかもしれません)。Windowsユーザの方は、Cocoという依存プログラムを拾ってこなければなりません。あと、パーサ定義をシェルスクリプトで自動生成しているので、その代わりのバッチファイルも書かなきゃダメですね、多分。
Coco/R for C#からCoco.exeを選択してバイナリを拾ってきたら、cloneしたディレクトリ/Expresso/配下に配置してください。Expressoが言語のコアを担うプロジェクトです。バッチファイルは、cloneしたディレクトリ/Expresso/parserCompile.shというシェルスクリプトを参考に作成していただきたいのですが、Coco.exeに渡すオプションは自由に変更してください。バッチファイルを作成したら、Expressoプロジェクト設定でビルド前に自動実行するように設定すれば、いちいち手動でパーサを生成する必要がなくなります。
長々と書きましたが、Windowsでの動作確認は不十分な(動きはするもののテストは通らない程度までしか確認していない)ので、動くことは保証しません。手軽に使いたいなら、Mac+Visual StudioかLinux+Xamarin Studioあたりの環境をお勧めします(昔は、後者、今は前者の開発環境で作ってます)(2018/4/7 追記: 現状、Windowsでは.NET自体の実装が異なるようでEmitterTestsが動きません)(2018/4/8 追記: Windowsでも、大方のEmitterTestsを動かせるようになりました。ですが、動かないものは割とどうしようもなさそうなエラーで失敗してます・・・特にOOPのはずなのに、インターフェイスが動かないのは痛すぎる・・・)。
さて、ここまでで肝心のコンパイラは動かせるようになったはずなので、伝統的なHello worldプログラムに移ります。Hello worldプログラムは、以下のように書きます。
中身の解説をする前に実行してみましょう。ExpressoConsoleプロジェクトをビルドし、MacかLinuxならmono exsc.exe hello_world.exs -o ./ -e hello_worldなどとしてまずコンパイルします。その後、カレントディレクトリにExpresso.dllとExpressoRuntime.dllをコピーしてください。正式リリースする頃には、この部分はなんとかすると思いますが、とりあえず今は、ランタイム環境が必要です。そして、できあがったmain.exeをmono hello_world.exeとして実行すると、Hello, world!と画面上に出力されるはずです。Expressoプログラムの実行に成功しました!
Expressoでは、基本的に1ファイル1モジュール構成を採用しています。ここは、Python譲りですね。各モジュールは、明示的に名前付けすることを義務付けられています。プログラムのエントリーポイントは、mainモジュールのmain関数からになります。今のところ、main関数は、引数も戻り値もなしの仕様になっています(Cのように文字列配列の引数を定義したり、intを戻り値にしても動きますが、単純に無視されます)(2018/4/11 追記: main関数がargs引数を取り、intを返せるようになりました。monoを使って実行するからです)。
ご覧の通り、関数、メソッドはdefキーワードで定義します。PythonやRubyで採用されている構文だったと思いますが、Rustのようにfunction由来のキーワードだとメソッド定義に違和感があるからです。Rustにはトレイトオブジェクトはあるものの、オブジェクトはないので、メソッドは存在しないはずです(追記: 公式ドキュメントでは、implブロックの関数をメソッドと呼んでいるようですので、これは間違いだったようです)。
この例ではどこにも明示されていません(というか変数宣言がない)が、型は後置です。その際、変数名と型の区切り記号には、(-という記号を使用します。これは、数学の∈に由来するExpresso独自の記号(のはず)です。あまり型は明示してほしくないという思想の元、入力しづらい2文字の記号を採用しています。Rustには似た記号を一元化してくれる機能があったと思いますが、Expressoには導入していないので、(-を∈と書いても、認識してくれませんので悪しからず。
関数、メソッドの戻り値を明示する場合は、Rustでも採用されている->記号を使います。関数の戻り値は、return文から推論するので、省略しても構いません(上のmain関数では省略しているが、voidに推論される)。
上記のプログラムで呼び出しているprintln関数は、組み込みの関数です。.NET環境のConsole.WriteLine関数を使用しているので、可変長の引数をとって、それをカンマ区切りで標準出力に出力します。他に、お尻に改行を追加しないprintや、第1引数にConsole.WriteLineに準じるフォーマット文字列を取るprintFormat関数(追記: string interpolation(日本語だと「文字列補完」ですかね)を実装したので、廃止しました)などが存在します。
いかがでしたでしょうか。以上で、オレオレ言語Expressoの導入は終わりです。あ、Expressoの骨格の説明などをしませんでしたが、機能詳細などは次回の記事で行いましょう。コンパイラ作りに興味のある方には、次の記事が参考になるかもしれませんね。
突然ですが、プログラマをされている皆様が生涯で何としても作り上げたいプログラムはなんでしょうか?これは私の願望も多分に含まれているのですが、恐らく一定数の方が、自作言語と答えるのではないでしょうか?
というわけで今回の記事は、自作言語のExpresso(エクスプレッソ)の紹介をします。4,5年前から開発しているオレオレ言語なんですが、最近、try,catchなども実装し、それなりに使える言語になってきたので、α版として公開することにしようかと思った次第です。とは言っても、専用のサイトはまだ用意しませんが。
Expressoという名前は、ExpressiveとEspressoからの造語です。表現力豊かに、かつエスプレッソ(コーヒー)一杯飲む間にでも開発できるような簡潔な言語を目指すという意味を込めています。また、言語のスローガンとして"Easy for beginners, elegant for enthusiasts"という標語も掲げています。「初学者には簡単に、熱狂者には華麗に」という意味ですね。この標語にはあえて、eで始まる単語を多用しています。これは「e(いい)を探す言語」というダジャレです。
Expressoは、オレオレ言語でありながら、普及、実用化させるならPascalのような教育用言語の地位を目指しています。そのために、先ほどの標語のような目標を掲げているわけです。つまり、初学者には簡単に書ける言語、しかしながら、習熟者にとっても、書きやすい言語を目指すということです。
なぜ、教育用言語を開発するのか不思議に思っているの方もいるかもしれないので、解説しておくと、私は大学時代にPascalという言語でプログラミングを学んだのですが、Wikipediaには教育用と書いてあるんですよ。200x年にしては時代錯誤的な構文とまだ設計に慣れていなかったせいもあって無事単位を落とし、翌年Cに変わったカリキュラムで再履したんですが、その現状を変えたいと思ったのがきっかけです。まあ、スピードとかを売りにしてもRustに敵うわけがないという本音もあります。
言語仕様としては、まだα版と銘打ってることもあって、かなりガバガバなところが多いんですが、基本は静的型付け、オブジェクト指向を基本とするマルチパラダイム言語になっています。一番強く影響を受けている言語が、Rustなので、Rustで採用されている仕様が結構入っています。
現状、Expressoは、.NET環境上でのみ動く言語になっています。理由は、.NETだと比較的ランタイム環境を整えるのも楽ですし、クロスプラットフォームで動くのが大きいです。まあ、私が、いちばん好きな言語がC#だからというのもありますが……
伝統的なHello worldプログラムの解説をする前に、導入方法を紹介しましょう。現状、専用のサイトがないので、Githubのリポジトリからcloneして導入していただく形になります。こちらからcloneしてください。cloneしたら、git submodule update --initを実行してください。依存リポジトリの解決が行われます(といっても、一つしかありませんが)。そして、cloneしたディレクトリ/ExpressoTest/配下にtest_executablesというディレクトリを作成してください。ここにテストで使用するバイナリが吐かれる設定になっているので、これがないとテストが実行できません。gitにこのディレクトリを追加できるのなら、追加したいところではあります。
2018/4/7 追記: それからソリューションをVisual Studioなどで開いて、InteroperabilityTestプロジェクトをビルドし、出力されたDLLファイルを/ExpressoTest/sources/for_unit_testsディレクトリに移動してください。
そうしたら、Mac,Linuxユーザの方なら、あとはメインのソリューションファイルをIDEで開けば、ビルドして実行できるはずです(NuGetを使って一部の依存プロジェクトをダウンロードするようになったので、自動で手元にないプロジェクトをダウンロードするよう、IDEの設定を変える必要があるかもしれません)。Windowsユーザの方は、Cocoという依存プログラムを拾ってこなければなりません。あと、パーサ定義をシェルスクリプトで自動生成しているので、その代わりのバッチファイルも書かなきゃダメですね、多分。
Coco/R for C#からCoco.exeを選択してバイナリを拾ってきたら、cloneしたディレクトリ/Expresso/配下に配置してください。Expressoが言語のコアを担うプロジェクトです。バッチファイルは、cloneしたディレクトリ/Expresso/parserCompile.shというシェルスクリプトを参考に作成していただきたいのですが、Coco.exeに渡すオプションは自由に変更してください。バッチファイルを作成したら、Expressoプロジェクト設定でビルド前に自動実行するように設定すれば、いちいち手動でパーサを生成する必要がなくなります。
長々と書きましたが、Windowsでの動作確認は不十分な(動きはするもののテストは通らない程度までしか確認していない)ので、動くことは保証しません。手軽に使いたいなら、Mac+Visual StudioかLinux+Xamarin Studioあたりの環境をお勧めします(昔は、後者、今は前者の開発環境で作ってます)(2018/4/7 追記: 現状、Windowsでは.NET自体の実装が異なるようでEmitterTestsが動きません)(2018/4/8 追記: Windowsでも、大方のEmitterTestsを動かせるようになりました。ですが、動かないものは割とどうしようもなさそうなエラーで失敗してます・・・特にOOPのはずなのに、インターフェイスが動かないのは痛すぎる・・・)。
さて、ここまでで肝心のコンパイラは動かせるようになったはずなので、伝統的なHello worldプログラムに移ります。Hello worldプログラムは、以下のように書きます。
module main;
def main()
{
println("Hello, world!");
}
中身の解説をする前に実行してみましょう。ExpressoConsoleプロジェクトをビルドし、MacかLinuxならmono exsc.exe hello_world.exs -o ./ -e hello_worldなどとしてまずコンパイルします。その後、カレントディレクトリにExpresso.dllとExpressoRuntime.dllをコピーしてください。正式リリースする頃には、この部分はなんとかすると思いますが、とりあえず今は、ランタイム環境が必要です。そして、できあがったmain.exeをmono hello_world.exeとして実行すると、Hello, world!と画面上に出力されるはずです。Expressoプログラムの実行に成功しました!
Expressoでは、基本的に1ファイル1モジュール構成を採用しています。ここは、Python譲りですね。各モジュールは、明示的に名前付けすることを義務付けられています。プログラムのエントリーポイントは、mainモジュールのmain関数からになります。今のところ、main関数は、引数も戻り値もなしの仕様になっています(Cのように文字列配列の引数を定義したり、intを戻り値にしても動きますが、単純に無視されます)(2018/4/11 追記: main関数がargs引数を取り、intを返せるようになりました。monoを使って実行するからです)。
ご覧の通り、関数、メソッドはdefキーワードで定義します。PythonやRubyで採用されている構文だったと思いますが、Rustのようにfunction由来のキーワードだとメソッド定義に違和感があるからです。Rustにはトレイトオブジェクトはあるものの、オブジェクトはないので、メソッドは存在しないはずです(追記: 公式ドキュメントでは、implブロックの関数をメソッドと呼んでいるようですので、これは間違いだったようです)。
この例ではどこにも明示されていません(というか変数宣言がない)が、型は後置です。その際、変数名と型の区切り記号には、(-という記号を使用します。これは、数学の∈に由来するExpresso独自の記号(のはず)です。あまり型は明示してほしくないという思想の元、入力しづらい2文字の記号を採用しています。Rustには似た記号を一元化してくれる機能があったと思いますが、Expressoには導入していないので、(-を∈と書いても、認識してくれませんので悪しからず。
関数、メソッドの戻り値を明示する場合は、Rustでも採用されている->記号を使います。関数の戻り値は、return文から推論するので、省略しても構いません(上のmain関数では省略しているが、voidに推論される)。
上記のプログラムで呼び出しているprintln関数は、組み込みの関数です。.NET環境のConsole.WriteLine関数を使用しているので、可変長の引数をとって、それをカンマ区切りで標準出力に出力します。他に、お尻に改行を追加しないprintや、第1引数にConsole.WriteLineに準じるフォーマット文字列を取るprintFormat関数(追記: string interpolation(日本語だと「文字列補完」ですかね)を実装したので、廃止しました)などが存在します。
いかがでしたでしょうか。以上で、オレオレ言語Expressoの導入は終わりです。あ、Expressoの骨格の説明などをしませんでしたが、機能詳細などは次回の記事で行いましょう。コンパイラ作りに興味のある方には、次の記事が参考になるかもしれませんね。
登録:
投稿 (Atom)