javascript/C言語/アセンブラを用い、 字句解析、構文解析、インタプリタ、コンパイラのプログラムをスクラッチから作りながら、 「プログラミング言語の作り方」を解説する。
プログラミング言語を作るときに、一番最初にやるべきことは、四則演算ではない。
hello worldだ。
言語仕様として、 以下のような、「"文字列"」と、「print関数」だけがある言語を作ろう。
自作言語の拡張子を「.3」とするため、「source.3」を作る。
内容はprint関数で"hello world"と表示するだけだ。
$ cat source.3 print("hello world")
インタプリタを作るにあたり、以下のファイル構成で実装した。 ひとつひとつ見ていこう。
ファイルから読み、字句解析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を実装するにあたり、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では、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,")"); }
その他のユーティリティ的な処理をまとめている。
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だけができる「プログラミング言語」だから、とても簡単に作れた。