プログラミング言語(インタプリタ)の作り方
目次
プログラミング言語はどうやって作るのか?
プログラミング言語を作りたい。
そう考えるのは自然なことです。
知的探求。
その結果として言語づくりに興味が向くのは真っ当なことでしょう。
この記事ではプログラミング言語、とりわけインタプリタ言語の作り方を解説します。
関連記事
インタプリタの仕組みをインタプリタ開発者が解説
インタプリタのエラーハンドリングの設計は最初のうちに
プログラミング言語の種類
プログラミング言語を作りたいとなった場合にまず理解しなくてはいけないこと。
それはプログラミング言語の種類です。
プログラミング言語には大きく分けて2種類あります。
1つがコンパイラ型言語、もう1つがインタプリタ型言語です。
コンパイラ型言語
インタプリタ型言語
これら2つの言語の種類の違いを把握しておきましょう。
この記事ではおもにインタプリタ型言語の作り方を解説します。
しかし基本的なところはコンパイラもインタプリタも同じです。
つまり応用が利くということです。
コンパイラ型
コンパイラ型言語はコンパイルを行う言語のことです。
コンパイルとはソースコードを機械語に翻訳することを言います。
コンパイラとはコンパイルを行うプログラムのことです。
機械語とは0と1が羅列された言語です。
これは実行ファイルになります。
0010100100101001 0010100100101001 0010100100101001 0010100100101001
コンパイラ型言語の代表例としてはまずC言語があげられます。
それからC++やRustなどもそうです。
C言語
C++
Rust
コンパイラ型言語は実行速度が速いのが特徴です。
↑の3つの言語はプログラミング言語の中でも最速の部類に入ります。
インタプリタ型
インタプリタ型言語はコンパイラ型と違い、ソースコードをプログラムが解釈して実行する形式の言語です。
コンパイラはソースコードを機械語に翻訳します。
しかしインタプリタはソースコードを機械語に翻訳しません。そのまま実行します。
これの代表的な言語としてはまずPythonが挙げられます。
それからRubyやPHPなどもそうです。
Python
Ruby
PHP
インタプリタ型言語はコンパイラ型と比べて実行速度が遅いのが特徴です。
しかしその代わりに動的型付けなど便利な機能が入ることが多いです。
富豪的プログラミングやツール制作に大変向いています。
インタプリタ言語を作るには?
それでインタプリタ型言語を作るにはどうしたらいいのでしょうか?
筆者はもう5年以上インタプリタ型言語を開発しています。
このノウハウをあなたにお伝えします。
まずインタプリタ開発で考えなくてはいけないことは、何の言語でインタプリタを作るか?
というところです。
何の言語でインタプリタを作るか?
ここをまず押さえておく必要があります。
これがすべての始まりです。
何の言語で作るか?
インタプリタはとにかく速度がネックになります。
ですのでインタプリタを開発する言語は速度が速い言語を選ぶ必要があります。
インタプリタでインタプリタを開発するのは習作にはいいですが実用的ではありません。
コンパイラでインタプリタを開発するのが一般的です。
つまり開発に使える言語はC言語やC++, それからRustなどの速い言語です。
C言語
C++
Rust
最近のメジャーな言語、たとえばPythonやRubyなどはC言語で作られています。
Node.jsなどはC++で作られていますね。
C言語は40代~60代の開発者に人気があった言語です。
ですのでその年代ぐらいの言語開発者はC言語で言語開発することが多いわけですね。
たとえばGo言語なども最初はC言語で実装されました。
その後Go言語はGo言語自体で実装されましたが。
C言語はコンパクトな言語ですがOSや言語開発では非常に威力を発揮する言語です。
そのため言語開発ではC言語を使う人が多いです。
若い開発者はC++やRustなどを好む傾向があるようです。
必要なのはとにかく速度
インタプリタ開発ではとにかく速度が必要とされます。
C言語やC++などの速い言語で実装する。
それが近道です。
この開発言語の選択を間違えるとその後の開発が困難になってしまいます。
特に速度については取り戻そうと思うと非常に大変です。
ですので速い言語で開発する必要があります。
インタプリタ言語の設計
インタプリタの設計はどうやったらいいのでしょうか?
これは有名な設計ツールがあります。
その名も「BNF」です。
BNF
BNFはバッカス・ナウア記法と呼ばれるものです。
これはプログラミング言語などの構造を設計するための記法です。
たとえば四則演算のBNFは↓のようになります。
expr: term [ ('+' | '-') term ]* term: factor [ ('\*' | '/') factor ]* factor: digit | '(' expr ')' digit: [0-9]+
↑の場合、expr
が「加算減算」に相当する要素です。
このexpr
の構成は「term [ ('+' | '-') term ]*
」になっています。
つまりterm
と、+
の-
のどちらか1つとterm
の0以上の繰り返しで構成されます。
そしてterm
も同様に定義されます。
これは「乗算除算」に相当する要素です。
そしてfactor
では整数かカッコでexpr
を再帰します。
じっさいの実装ではexpr
やterm
やfactor
は関数として実装されます。
そして関数内の処理が:
以降の内容に沿って実装されます。
インタプリタ言語の構成
設計ができたところでインタプリタの構成についてです。
インタプリタを構成するものを主に3つです。
それは
字句解析器
構文解析器
構文実行器
の3つになります。
字句解析器
字句解析はテキストの単語を分解する解析です。
たとえば
if (1 + 1) { }
というif文があったとするとこれは字句解析では
[ 'if', '(', '1', '+', '1', ')', '{', '\n', '}' ]
というリストに分解されます。
実際には文字列として表現するのではなくenumなどの定数で表現されます。
字句解析はこれに続く構文解析のための下準備です。
ここがバグってると後の処理も期待した動作をしないので注意が必要です。
字句解析器も最初はテストを書いた方がいいでしょう。
構文解析器
構文解析はBNFに従って字句を解析します。
目的は構文木の構築です。
構文木とは木構造の一種で文法を表現するための構造です。
たとえば「1 + 1」という構文は↓のような木構造になります。
add / | \ 1 + 1
構文木を構築する目的は構文実行器で構文を実行するため。
つまり言語として処理を実行するためです。
構文木自体はノードと呼ばれる構造体などで作られていきます。
ノードから別のノードにポインタなどでリンクを伸ばし木構造を構築します。
構文実行器
構文実行は構築されている構文木に従って実際に文や式を実行していきます。
ここで実際に整数や実数、クラスのインスタンスなどのオブジェクトが作成されていきます。
足し算や引き算が実際に行われるのもここです。
インタプリタ言語の難しさとは?
それでインタプリタ言語の開発の難しさとは何があるのでしょうか?
私が思うにインタプリタ言語の開発は↓の点が難しいと思います。
メモリリークのフィックス
テストの網羅性
メモリリークのフィックス
C/C++系の言語で開発するとメモリリークというバグに悩まされることがあります。
メモリリークとは動的なメモリの確保で確保したメモリを開放していない時に発生するバグです。
つまりメモリリークとは「メモリの解放漏れ」のことです。
メモリリークはメモリの解放漏れ
このメモリリークは長時間稼働でネックになります。
プログラミング言語は長時間稼働するプログラムなのでメモリリークのバグがあると大変です。
バグがある言語を長時間稼働するとメモリをどんどん消耗してパソコンを不安定にさせてしまいます。
メモリリークはValgrindなどのメモリチェックツールで検出できます。
C/C++系の言語で開発するときはこのメモリリークに注意しましょう。
テストの網羅性
プログラミング言語はとても複雑なプログラムです。
もちろんテストは必須です。
テストとは関数などが期待した動作をするかチェックすることを言います。
プログラミング言語はテストがいくらあっても足りないです。
そのためテストをたくさん書く必要があります。
これを網羅的に書くのはほぼ不可能です。
ですのでテスト方法についても考えていく必要があります。
おわりに
今回はプログラミング言語の作り方を解説しました。
インタプリタの開発は言語開発の入門にうってつけだと思います。
興味がある方は挑戦してみてください。
(^ _ ^) | インタプリタを開発しよう |
(・ v ・) | 処理系作れるの?すげー |