プログラミング言語の作り方

javascript/C言語/アセンブラを用い、 字句解析、構文解析、インタプリタ、コンパイラのプログラムをスクラッチから作りながら、 「プログラミング言語の作り方」を解説する。

hello world
javascript版プログラミング言語の作り方(インタプリタ開発)

プログラミング言語を作るときに、一番最初にやるべきことは、四則演算ではない。

hello worldだ。

ページメニュー

現時点の言語仕様

言語仕様として、 以下のような、「"文字列"」と、「print関数」だけがある言語を作ろう。

自作言語で記述されたソース

自作言語の拡張子を「.3」とするため、「source.3」を作る。

内容はprint関数で"hello world"と表示するだけだ。

$ cat source.3
print("hello world")

インタプリタのファイル構成

インタプリタを作るにあたり、以下のファイル構成で実装した。 ひとつひとつ見ていこう。

interpretor.jsの実装

ファイルから読み、字句解析lexerで、tokens配列を取得。 tokensの内容を表示してから、parserに渡して終了だ。

なお、簡略化のため、parser内部で即実行までしている。

$ cat interpretor.js
#!/usr/bin/node

//別ファイルに記述している処理を読み込む
var {read,show}  = require("./utils.js");
var lexer = require("./lexer.js");
var parser = require("./parser.js");

//source.3から読み込む
var source = read("source.3");

//字句解析
var tokens = lexer(source);
show("tokens =",tokens);

//------構文解析しながら実行------//
parser(tokens);

lexer.jsの実装

字句解析lexerを実装するにあたり、javascriptの正規表現を使っている。

トークン配列tokensに分割することは、まさに、文字列を分割することなので、splitに正規表現を適用している。 分かりやすくするため、複雑な正規表現を用いていない。

$ cat lexer.js
//------字句解析------//
module.exports = function(source){
    //正規表現を使い、"文字列" or print or 改行で分割。
    //丸カッコで囲まれると残り、囲まれていないと捨てられる
    var tokens = source.split(/(".*"|print)|\n/);

    //splitの仕様上、undefinedや''などが残るので、不要なものは捨てる
    tokens = tokens.filter(a=>a);

    return tokens;
}

parser.jsの実装

構文解析parserでは、callprintがメインだ。

callprintでは、文法規則にそって、 「関数名("文字列")」の順番どおりになっているか 確認し、問題なければ、その場で「文字列」の部分を表示(=実行)している。

$ cat parser.js
module.exports = parser;
var {expect,accept,show,error}  = require("./utils.js");

var tokens;

//構文解析開始
function parser(t){
    tokens = t;
    return callprint();
}

//print関数呼び出しの構文解析
function callprint(){
    if(tokens.length==0)return;

    //関数名がprintであること
    expect(tokens,"print");

    //関数呼び出しの丸カッコであること
    expect(tokens,"(");

    //文字列を取得
    var msg = tokens.shift();
    //ダブルクォーテーションを取り除く
    msg = msg.substr(1,msg.length-2);
    //表示=即実行
    console.log(msg);

    //閉じカッコであること
    expect(tokens,")");
}

utils.jsの実装

その他のユーティリティ的な処理をまとめている。

accept関数は、引数に指定したものがtokensの先頭にあれば返し、無ければスルーして無視する。 expect関数は、そのエラー版だ。

$ cat utils.js
//---------------ユーティリティ関数定義---------------//
module.exports = {read,show,error,accept,expect};

//ファイルを読み込む
function read(filename){
    return require('fs').readFileSync(filename,"utf-8");
}

//深い階層まですべて表示する。
function show(msg,obj){
    obj = require('util').inspect(obj,{
              showHidden: false, depth: null, maxArrayLength: null,colors: true
          });
    console.log(msg + obj);
}

//エラーメッセージを表示して終了する
function error(...msgs){
    console.log(msgs.join(""));
    process.exit();
}

//tokens[0]が指定されたcsに含まれるならshiftして返す。そうでないなら何もしない
function accept(tokens,...cs){
    //tokensがもうない
    if(tokens.length==0) return;
    //tokensの先頭が、指示されたcsに含まれるので、shiftして返す
    if(cs.includes(tokens[0])) return tokens.shift();
    //なにもしない
    return;
}

//acceptのエラー版
function expect(tokens,...cs){
    var t = accept(tokens,...cs);
    //取得できたらそのまま返す
    if(t) return t;
    //エラー表示して終了。
    error("tokens[0]=",tokens[0],"が",cs,"に含まれないため終了。");
}

インタプリタの実行

interpretor.jsに実行権限を付けて、実行する。

まず、tokensが意図どおりに分割されているか、念のため表示しているので、確認しよう。

その後、「hello world」と表示されており、 問題なく、自作プログラミング言語を実装できていることが分かる。

$ chmod 755 interpretor.js
$ ./interpreter.js
tokens =[ 'print', '(', '"hello world"', ')' ]
hello world

hello worldだけができる「プログラミング言語」だから、とても簡単に作れた。


このページの目次へ戻るサイトの最上位へ戻る