JavaScript
プログラミング
言語作成
プログラミング言語
自作

まったりプログラミング言語自作したい 〜 (7/24 whileとか代入とかなんか出来た)

前書き

 みなさん、こんにちは。突然ですが、プログラム言語や、コンパイラを、自分で作ってみたいと思ったことはありませんか。

 私はあります。というわけで、さっそく作っていきましょう!私は自作初心者なので、筋の悪いところや間違っている所などありましたら、指摘いただけると嬉しいです。

目標

 プログラム言語を作るというからには、文法を決めることが必要です。さらに、その文法に従って作られたプログラムは、実行出来ないといけません。文法はどのプログラム言語にもありますが、実行方式は様々です。

 たとえば、C言語は機械語というCPUへの命令を生成して、その機械語を実行します。他の言語では、機械語を生成せずに命令を実行するものもあります。もちろん、真に機械語を生成しないで命令を実行するのは不可能です。ここでいう機械語を生成しないとは、他の機械語を生成できるようなプログラム言語の機能を呼び出すことで、自分自身は機械語を生成しないというものです。

 いきなり機械語を生成する本格的なコンパイラを作るのは無理があると思いますし、機械語はCPUごとに違うので、勉強も大変です。なので今回は、既存の他のプログラム言語の上で動く、自作言語をまずは目指したいと思います。

 しかし、私はもっと細かい所や、より低いレイヤーでの、プログラム言語についても分かりたいので、欲を言えば機械語やアセンブリ言語にも手を出してみたいと思っています。これを踏まえて、JavaScriptを実行環境として、その上に自作言語を作ろうと思いました。

なぜJavaScriptか

  • JavaScriptは、ブラウザさえあれば動くから。どうせなら色んな環境で動く言語にしたい。
  • JavaScriptは、高水準な言語だし、開発者ツールもブラウザに最初から付いてるし、便利で手軽。
  • JavaScriptには、なんとアセンブリ言語もある。(wasm)なので、最終的にアセンブリ言語に変換したいなー作りたいなーと思ったら作れると思う。

 どこでも動くという利点に、高度な開発者ツールもあり、しかもアセンブリ言語、つまりはほとんど機械語のようなものを実行できる環境さえある。これで決まりだ!

ソースコード構造 〜仕様策定に向けて〜

 皆さん、データ構造とアルゴリズムについて、ある程度詳しいですか?

 問題を解決するには、適切なデータ構造が必要です。例えば生徒10人のテストの成績の平均点を出したいとします。

 そういった時、例えば配列のようなデータ構造で情報が存在したら、楽に計算できますよね。

var score = [90, 85, 60, 70, 55, 100, 80, 70, 80, 50];

 なぜなら、配列は数を使って各要素にアクセス出来るので、数字を一つずつ増やすことで、生徒全員に順番にアクセスできるからです。アクセスした上で全員足せば合計となり、それを10で割れば平均になります。

 配列には「.length」というメンバ変数も含まれるので、この10という数字も簡単に取得出来ます。そうすると、10人どころか、生徒n人に対応する平均点が簡単に計算できます。

var sum = 0;
for (var i = 0; i < score.length; i++) {
  sum += score[i];
}

console.log(sum / score.length);

 しかし、もし与えられたデータが、こんな感じだったらどうでしょう。

var score = "90点 85点、60点、 70点、55点、百点満点、80,  70, 80 50";

 恐ろしいデータです。全ての情報が単一の文字列になっているので、添字を使ってそれぞれの点数にアクセス出来ません。

 しかも、文字列なのでそもそもまず数字に変換する必要がありますが、"点"などの余分な情報が困難さを上げています。"点"を全て削除して対応しようとすると、"百点満点"が"百満"になって、謎の単語が生まれてしまったりします。

 それぞれの点数が、"、"で区切られていたり、","で区切られていたりして、簡単にはそれぞれの生徒ごとに区切らせてくれません。数字も全角だったり半角だったりしますし、百点満点に至っては漢字です。とにかく非常に扱いづらいデータだと言えるでしょう。

 この例で伝えたかったのは、データ構造の素晴らしさです。配列という、連続して要素にアクセスする際に有用なデータ構造と、そういった要素をまったく考えていない上の文字列を比較すれば分かると思います。

 しかし、プログラム言語を作って、コンパイラを作って、という行為には、こういった難しさがあります。ソースコードは、最初、文字列の塊でしかありませんから。

 自作言語を作るに当たっても、その処理をする時には、ソースコードから扱いやすいデータ構造に変換する必要があります。そしてそのデータ構造から、実際の計算をしたり、アセンブリや機械語を生成する、という段階を踏むべきなのです。

ソースコード → 中間表現(データ構造) → 最終的な生成物(機械語や、直接実行出来る形式)
 
 また、データ構造は「処理しやすい」形式である必要がありますし、ソースコードの文法も、そういったデータ構造に「変換しやすい文法」である方が、便利です。そのため、まず決めるべきは文法ではなく、中間表現のデータ構造だと考えられます。自分の好きな文法を何も考えず作ると、それがコンピュータにとって処理しやすいかということが抜け落ちて、いざコンパイラを作ろうと思った時に、非常に困難になると思います。

 それは、上記の生徒ごとの成績の例から明らかですね。こんな自由気ままな書式ではいけないということです。

// 腹が立つくらい自由気ままな書式の例
var score = "90点 85点、60点、 70点、55点、百点満点、80,  70, 80 50";

 では、どういう中間構造を作れば良いか、今回でいえば、「JavaScript上で処理しやすいデータ構造で、そのデータ構造は、プログラム実行時に必要な要素を網羅することが出来、アクセスも簡単」という感じでしょうか。プログラムにとって必要な要素とは、例えば値を表現出来たり、式を表現出来たり、文を表現できたり、ということです。

 私はそういった中間表現のデータ構造を勝手にソースコード構造と読んでいます。具体的には、例えば以下のようなものです。これは私が考えたもので、私がいま作成中の自作言語の中間表現です。JSONの表記法を使って、必要な要素を整理して記述してあります。

var program = {
  "add": {
    param: ["a", "b"],
    block: [{
      type: "return",
      exp: [{
        type: "eval",
        value: 'locals["a"] + locals["b"]'
      }]
    }]
  }
}

 これは、加算を実行する関数の定義で、JavaScriptで言えばこういう関数に対する中間表現です。

function add(a, b) {
  return a + b;
}

 こういったデータ構造を作れば、"add"という関数が呼ばれた時に、program["add"]でその関数の定義を呼び出せます。
 さらに、paramにアクセスして仮引数名をとりだして、その変数名でその関数内でのみ有効なローカル変数を作成し、その上でblockの中身を実行していって、・・・という風に処理を進めていくことが出来ます。

 このように、情報を取り出しやすいデータ構造を決めれば、それを元にした処理がしやすいです。さらに、こういったデータ構造を生成しやすい文法を考えることで、文法の仕様も決められます。

 その仕様を元に、中間表現から実際の命令を行う処理や、ソースコードから中間表現を作成する処理を書いていけば、プログラム言語が自作できる気がしてきませんか?

プログラミング言語に必要な5つの要素とは

 突然ですが、コンピュータの五大装置というものがあります。基本情報技術者試験などで問われる知識なので、知っている人も多いでしょう。コンピュータの五大装置とは、

  • 演算装置
  • 記憶装置
  • 制御装置
  • 入力装置
  • 出力装置

の5つのことです。現行のコンピュータは、この五大装置によって成り立っているそうです。プログラミング言語というのは、コンピュータがどのように動作するかを指し示すものであるはずです。なので、プログラミング言語にも、この五大装置に対応する要素が必要ではないかと思います。そういった考えのもと、プログラミング言語を設計していこうと思います。具体的には

  • 演算装置 -> 式
  • 記憶装置 -> 変数
  • 制御装置 -> 制御文
  • 入力装置 -> 引数
  • 出力装置 -> 戻り値  

 という対応付けを行います。この5つの要素が全てそろえば、プログラミング言語としては十分だと考えます。では、まずは「式」からその仕様決定と実装を行っていこうと思います。

式とは 

 この記事の中で言う式とは、値及び、評価すると値になるもののこととします。評価とは、関数に引数を与えて戻り値を返すことです。例えば、以下はいずれも式です。

// 値は式
5;
//値をもとに計算したものも式
3 + 5;
//関数を実行して値が返ってくるなら式
plus(3, 5);

function plus(a, b) {
  return a + b;
}

処理しやすい式とは

 ソースコード構造で、まずコンピュータに処理しやすいデータ構造を決めるべきだ!という考えを述べました。なので、どのような形式で式を表せば、コンピュータによって処理しやすいかを考えて、仕様を決めます。今回は、逆ポーランド記法によって式を表したいと思います。逆ポーランド記法とは、ざっくり言うと値の後に演算子(関数)を書く形式のことです。詳しくはwikipediaをどうぞ。例としては、下記の通りです。

通常の関数呼び出しの例:
plus(3, 5)
逆ポーランド記法の例:
5 3 plus
通常の四則演算の例:
3 + 5
逆ポーランド記法の例:
5 3 +
 
 逆ポーランド記法で式を表すと、非常に計算しやすくなります。具体例で説明します。

3 - 5 * 2
これを逆ポーランド記法に直したもので説明します。
2 5 * 3 -

上の普通の式の場合、人間に取って読みやすいですが、それを計算するときは、掛け算を先に計算する、という優先順位に気をつけて計算をする必要があります。対して、下の逆ポーランド記法では、優先順位はまったく気にせず、左から順番に計算していくことで答えを出すことが出来ます。なぜかというと、正しい逆ポーランド記法ならば、優先順位の高い計算は、低い計算よりも早く現れるよう書いてあるはずなので、ただ左から読み込んで計算するだけで良いからです。なので、下記の手順でこの計算が出来ます。

  1. 処理開始、スタックを用意する。
    • []
  2. 先頭要素を読む。2が出たので、スタックに追加する。
    • [2]
  3. 次の要素を読む。5が出たので、スタックに追加する。
    • [2, 5]
  4. 次の要素を読む。*が出たので、スタックの後ろから2つ取り出す。5と2が取り出され、5 * 2の結果をスタックに追加する。
    • [10]
  5. 次の要素を読む。3が出たので、スタックに追加する。
    • [10, 3]
  6. 次の要素を読む。-が出たので、スタックの後ろから2つ取り出す。3と10が取り出され、3 - 10の結果をスタックに追加する。
    • [-7]
  7. 全ての要素を読み終わったので、計算終了する。スタックに残った-7が計算結果となる。

 このように、数値が現れたら追加し、演算子が現れたら計算をする。それを先頭から繰り返すだけで計算できます。コードは以下の通りです。

// 演算子名、関数名をキーに、その引数の長さと、処理を記述する。
var functions = {
  // 加算
  "+": {
    length: 2,
    body: params => params[0] + params[1]
  },
  // 減算
  "-": {
    length: 2,
    body: params => params[0] - params[1]
  },
  // 掛け算
  "*": {
    length: 2,
    body: params => params[0] * params[1]
  },
  // 割り算
  "/": {
    length: 2,
    body: params => params[0] / params[1]
  }
};

// 逆ポーランド記法で記述された式を受け取って計算する。
function calc_rpn(rpn_list) {
  // 処理用スタック
  var stack = [];

  // 式を左から順番に見ていく。
  for (var i = 0; i < rpn_list.length; i++) {
    // 現在見ている要素
    var e = rpn_list[i];

    if (!(e in functions)) {
      // 要素が関数名でなかったら、数値と判断してスタックにpushする。
      stack.push(parseFloat(e));
    } else {
      // 要素が関数名だったら、その関数名に従って計算する。

      // 引数の数だけ、スタックから取り出す。
      var args = [];
      for (var j = 0; j < functions[e].length; j++) {
        args.push(stack.pop());
      }

      // 引数を元に計算し、その結果をスタックにpushする。
      stack.push(functions[e].body(args));
    }
  }

  // スタックから値を取り出してreturn
  return stack.pop();
}

// 逆ポーランド記法で記述された式 3 - 5 * 2
var rpn_list = ["2", "5", "*", "3", "-"];

// -7
console.log(calc_rpn(rpn_list));

 逆ポーランド記法で書かれた式を計算することが出来ました。次は、通常の関数呼び出しの形、例えば「plus(1, 2)」などで書かれた式を逆ポーランドに変換したいと思います。

関数呼び出しを逆ポーランド記法に変換

 驚くべきことに、通常の関数呼び出しを逆順にするだけで、逆ポーランド記法になります。(たぶん)「plus(1, 2)」は、カンマや括弧を無くした上で、ただ逆順にするだけで逆ポーランド記法になります。つまり「2 1 plus」です。
 同様に、minus(3, mul(5, 2))という関数呼び出しがあったとすると、それを逆から読んだ「2 5 mul 3 minus」が逆ポーランド記法になります。ちなみにmulが掛け算で、minusが引き算だとすると、これは「2 5 * 3 -」となります。どこかで見た式ですね。

 というわけで、普通の関数呼び出しを逆ポーランド記法に変換するアルゴリズムは驚くほど単純なので、コードを載せても退屈になるので載せません。

中置記法を逆ポーランド記法に変換

 中置記法とは、四則演算などで普通に使われる、演算子を被演算子の間に置く記法のことです。ようは「3 + 5」のような書き方のことです。これを逆ポーランド記法に変換してみます。しかし、ググった結果、なかなか難しそうなやり方しか見つかりません。操車場アルゴリズム、演算子順位解析、再帰下降構文解析など、いろいろ調べたのですが、どれも複雑な処理でした。というか良く分からない・・・。
 そもそも直感的には、そんなに複雑なアルゴリズムを必要とする問題でもない気もします。自作なので自分で考えるのも醍醐味ということで、まったり考えた結果、浮かんできたアイデアがこちらです。(伝われ)

3 - 5 * 2

    ↓ 優先順位が低い奴を高い位置に上げる。

  - 
3   5 * 2

    ↓ 取り残された奴らを括弧でくくる。
   -
(3) (5 * 2)

    ↓ 高い位置にある演算子を一番左に持ってくる。

- (3) (5 * 2)

    ↓ 括弧の中でもこの処理を繰り返す。

         *
- (3) (5   2)
    ↓

          *
- (3) ((5) (2))
    ↓

- (3) (*(5)(2))

    ↓ 括弧の中から演算子が無くなったら、逆順にして処理を終了する。

2 5 * 3 -

 演算子を見つけたら、それを左に持っていくことでポーランド記法に変えていく直感的なアルゴリズムです。演算子はより優先度の低い演算子から先に処理対象とします。最後に反転させて逆ポーランド記法に直します。今回はこのアルゴリズムで試しに実装してみることにします。(駄目だったら素直にもう少し勉強してから出なおして来ます。)

 もう少し細かく実装を詰めます。上記の処理に加えて、演算子が左結合で同等の優先度の場合は、より右にある演算子を処理対象にします。演算子が右結合で同等の優先度の場合は、左にある演算子を処理対象にします。また、単項演算子が見つかった場合は、演算子の移動は行いません。

// 中置記法をパースしてポーランド記法に変換する。
function toPN(exp) {
  // 演算子を優先度の昇順で並べる。
  var opes = [
    ["="],
    ["==", "!="],
    ["<", ">"],
    ["+", "-"],
    ["*", "/", "%"],
    ["**"],
    ["!"]
  ];

  // 右結合する演算子
  var right_opes = ["=", "**", "!"];
  // 単項演算子
  var u_opes = ["!"];

  for (var i = 0; i < opes.length; i++) {
    // opes[i]の演算子が現れるか調べる。iが小さい演算子ほど優先順位が低い。
    var ope_dis = []; //演算子が現れるまでの距離
    for (var j = 0; j < opes[i].length; j++) {
      if (right_opes.indexOf(opes[i][j]) == -1) {
        // 右から調べることで、左結合になる。   
        var index = exp.lastIndexOf(opes[i][j]);
        ope_dis.push(exp.length - 1 - index);
      } else {
        // 左から調べることで、右結合になる。
        var index = exp.indexOf(opes[i][j]);
        if (index == -1) {
          ope_dis.push(exp.length);
        } else {
          ope_dis.push(index);
        }
      }
    }

    // 同じ優先順位の演算子の中で、見つかるまでの距離が一番小さいものを選択。
    var min_dis = Math.min(...ope_dis);

    // min_disがexp.lengthだったら、今見ている優先度では演算子が見つからなかったので、処理しない。
    if (min_dis < exp.length) {
      // 見つかるまでの距離が小さい演算子を取得
      var k = ope_dis.indexOf(min_dis);
      var operator = opes[i][k];

      // 演算子が見つかった位置を計算
      var operator_index;
      if (right_opes.indexOf(operator) == -1) {
        operator_index = exp.length - 1 - min_dis;
      } else {
        operator_index = min_dis;
      }

      if (u_opes.indexOf(operator) == -1) {
        // 二項演算子なら、演算子を左に移動させる。
        var res = [];
        res.push(operator); //演算子をpushする。
        res.push(...toPN(exp.slice(0, operator_index))); // 演算子より左側の式をパースしてポーランド記法に直す。
        res.push(...toPN(exp.slice(operator_index + 1, exp.length))); // 同様に演算子より右側の式をパースする。

        return res;
      } else {
        // 単項演算子なら、演算子を移動させない。
        var res = [];
        res.push(...toPN(exp.slice(0, operator_index))); // 演算子より左側の式をパースしてポーランド記法に直す。
        res.push(operator); //演算子をpushする。
        res.push(...toPN(exp.slice(operator_index + 1, exp.length))); // 同様に演算子より右側の式をパースする。

        return res;
      }
    }
  }

  // 演算子が一つも見つからなかったら、終了と判断してそのまま式をリターン。
  return exp;
}

// 3 - 5 * 2
var words = ["3", "-", "5", "*", "2"];
// ポーランド記法に変換
var exp = toPN(words);
// 逆ポーランド記法に変換
exp.reverse();
// -7
console.log(calc_rpn(exp));

演算子12種類をひとまず対象としました。少しテストケースを書いてみます。φ(。_。*)カキカキ
最初は手で式を書こうと思いましたが、大変なので、総当りに近い形で2万件くらいの式をfor文で生成して、それを実行してみました。大丈夫そうです。
test_201807211737.js

 ただ、これだと括弧で優先順位を上げることに対応してませんので、それに対応する必要がありますが、今回はここまででひとまず終わり!

なんかwhileとか代入とか出来るようになった

後でコメント書きます。

// 演算子名、関数名をキーに、その引数の長さと、処理を記述する。
var functions = {
  // 加算
  "+": {
    length: 2,
    body: params => params[1] + params[0]
  },
  // 減算
  "-": {
    length: 2,
    body: params => params[1] - params[0]
  },
  // 掛け算
  "*": {
    length: 2,
    body: params => params[1] * params[0]
  },
  // 割り算
  "/": {
    length: 2,
    body: params => params[1] / params[0]
  },
  // 余り
  "%": {
    length: 2,
    body: params => params[1] % params[0]
  },
  // 累乗
  "**": {
    length: 2,
    body: params => params[1] ** params[0]
  },
  // 等しい
  "==": {
    length: 2,
    body: params => params[1] == params[0]
  },
  // 等しくない
  "!=": {
    length: 2,
    body: params => params[1] != params[0]
  },
  // 小なり
  "<": {
    length: 2,
    body: params => params[1] < params[0]
  },
  // 大なり
  ">": {
    length: 2,
    body: params => params[1] > params[0]
  },
  // NOT
  "!": {
    length: 1,
    body: params => !params[0]
  },
  // 標準出力
  "print": {
    length: 1,
    body: params => console.log("stdout", params[0])
  }
};

// 中置記法をパースしてポーランド記法に変換する。
function toPN(exp) {
  // 演算子を優先度の昇順で並べる。
  var opes = [
    [";"],
    ["="],
    ["==", "!="],
    ["<", ">"],
    ["+", "-"],
    ["*", "/", "%"],
    ["**"],
    ["!"]
  ];

  // 右結合する演算子
  var right_opes = ["=", "**", "!"];
  // 単項演算子
  var u_opes = ["!", ";"];

  for (var i = 0; i < opes.length; i++) {
    // opes[i]の演算子が現れるか調べる。iが小さい演算子ほど優先順位が低い。
    var ope_dis = []; //演算子が現れるまでの距離
    for (var j = 0; j < opes[i].length; j++) {
      if (right_opes.indexOf(opes[i][j]) == -1) {
        // 右から調べることで、左結合になる。   
        var index = exp.lastIndexOf(opes[i][j]);
        ope_dis.push(exp.length - 1 - index);
      } else {
        // 左から調べることで、右結合になる。
        var index = exp.indexOf(opes[i][j]);
        if (index == -1) {
          ope_dis.push(exp.length);
        } else {
          ope_dis.push(index);
        }
      }
    }

    // 同じ優先順位の演算子の中で、見つかるまでの距離が一番小さいものを選択。
    var min_dis = Math.min(...ope_dis);

    // min_disがexp.lengthだったら、今見ている優先度では演算子が見つからなかったので、処理しない。
    if (min_dis < exp.length) {
      // 見つかるまでの距離が小さい演算子を取得
      var k = ope_dis.indexOf(min_dis);
      var operator = opes[i][k];

      // 演算子が見つかった位置を計算
      var operator_index;
      if (right_opes.indexOf(operator) == -1) {
        operator_index = exp.length - 1 - min_dis;
      } else {
        operator_index = min_dis;
      }

      if (u_opes.indexOf(operator) == -1 || right_opes.indexOf(operator) == -1) {
        // 二項演算子か単項演算子の左結合なら、演算子を左に移動させる。
        var res = [];
        res.push(operator); //演算子をpushする。
        res.push(...toPN(exp.slice(operator_index + 1, exp.length))); // 同様に演算子より右側の式をパースする。
        res.push(...toPN(exp.slice(0, operator_index))); // 演算子より左側の式をパースしてポーランド記法に直す。

        return res;
      } else {
        // 単項演算子かつ右結合なら、演算子を移動させない。
        var res = [];
        res.push(...toPN(exp.slice(0, operator_index))); // 演算子より左側の式をパースしてポーランド記法に直す。
        res.push(operator); //演算子をpushする。
        res.push(...toPN(exp.slice(operator_index + 1, exp.length))); // 同様に演算子より右側の式をパースする。

        return res;
      }
    }
  }

  // 演算子が一つも見つからなかったら、終了と判断してそのまま式をリターン。
  return exp;
}

// 丸括弧の含まれる中置記法を解析する。
function parse(exp) {
  // 処理用スタック
  var stack = [];

  // 式を左から順番に見ていく。
  for (var i = 0; i < exp.length; i++) {
    // 現在見ている要素
    var e = exp[i];

    if (e != ")") {
      // 閉じ括弧が現れるまでは、そのままpushする。
      stack.push(e);
    } else {
      // 閉じ括弧が現れたら、その括弧の中身をパースしてから、スタックに再追加する。
      //開き括弧のインデックス
      var index = stack.lastIndexOf("(");

      // 括弧の中身を取り出す
      var ex = stack.slice(index + 1, stack.length);
      // スタックから括弧を削除する。
      stack = stack.slice(0, index);
      // 括弧の中身をパースしてから、スタックにpushする。
      stack.push(toPN(ex));
    }
  }

  // パースしてからリターン
  return toPN(stack);
}

// 多次元配列を一次元配列に変換
function flatten(list) {
  var res = [];
  for (var i = 0; i < list.length; i++) {
    if (Array.isArray(list[i])) {
      res.push(...flatten(list[i]));
    } else {
      res.push(list[i]);
    }
  }
  return res;
}


// 実行環境、関数定義を受け取る
function Env(func) {
  // ローカル変数
  this.locals = {};
  // 関数定義
  this.func = func;
  // 処理用スタック
  this.stack = [];
};
Env.prototype = {
  // ソースコードを受け取って実行する。
  execute: function(src) {
    // 字句で区切る。改行はセミコロンとする。
    var words = src.split("\n").join(" ; ").split("(").join(" ( ").split(")").join(" ) ").split(" ").filter(x => x.length > 0);
    // ポーランド記法に変換
    var rpn_list = flatten(parse(words));
    //逆ポーランド記法に変換
    rpn_list.reverse();

    console.log(rpn_list);

    // 式を左から順番に見ていく。
    for (var i = 0; i < rpn_list.length; i++) {
      // 現在見ている要素
      var e = rpn_list[i];

      if (e in this.func) {
        // 要素が関数名だったら、その関数名に従って計算する。

        // 引数の数だけ、スタックから取り出す。
        var args = [];
        for (var j = 0; j < this.func[e].length; j++) {
          var top = this.stack.pop();
          args.push(top in this.locals ? this.locals[top] : top);
        }

        // 引数を元に計算し、その結果をスタックにpushする。
        this.stack.push(this.func[e].body(args));

      } else if (e == "=") {
        // 代入式が現れた場合
        // 値を取り出す。
        var value = this.stack.pop();
        // ローカル変数に代入する
        this.locals[this.stack.pop()] = value;
        // 式なので、値を返す。
        this.stack.push(value);
      } else if (e == "if" || e == "while") {
        // if文かwhile文が現れた場合、条件に合致しなければend_ifおよびend_whileまで処理を飛ばす。
        var condition = this.stack.pop();
        if (!condition) {
          while (("end_" + e) != rpn_list[++i]);
        }

      } else if (e == ";" || e == "end_if") {
        // 何もしない
      } else if (e == "end_while") {
        // 前回whileまで戻す。
        while (--i >= 0 && "while" != rpn_list[i]);
        // さらにその前のセミコロンまで戻す。
        while (--i >= 0 && ";" != rpn_list[i]);

      } else if (/^([0-9]|[1-9][0-9]+)(|\.[0-9]|\.[0-9]+[1-9])$/.test(e)) {
        // 数値リテラルを正規表現で判断して、数値ならスタックに入れる。
        this.stack.push(parseFloat(e));
      } else {
        // それ以外なら、そのまま変数名をpushする。
        this.stack.push(e);
      }
    }

    // スタックから値を取り出してreturn
    return this.stack.pop();
  }
};

var src = "" +
  "x = 1 ; " + "\n" +
  "while ( x < 100 )" + "\n" +
  "  x = x + 5" + "\n" +
  "  print x" + "\n" +
  "end_while";
console.log(src);

var env = new Env(functions);
env.execute(src);

後書き

 前からプログラミング言語を作ってみたいな〜(思うだけ)でしたのですが、今回ついに挑戦しようと思ってしまいました。少しずつ作っていって、この記事も更新しようと思います。