🌟

JavaScriptで作る!ミニ言語のインタプリタ(4)〜組み込み関数

2024/09/30に公開

はじめに

前回の記事では、静的スコープの実装を行い、より直感的で予測可能な挙動のインタプリタを作ることができました。今回は、さらに機能を拡張し、入出力や数学的な操作を行う組み込み関数を実装していきます。

組み込み関数の必要性

現在のインタプリタでは、ユーザーが定義した関数しか使用できません。しかし、実用的なプログラミング言語には、入出力や数学的な操作など、言語自体に組み込まれた便利な関数が存在します。これらの関数を「組み込み関数」と呼びます。組み込み関数を実装することで、以下のようなメリットがあります:

  • 基本的な機能をユーザーが毎回実装する必要がなくなる
  • 最適化された実装を提供できる
  • 言語の表現力が向上する

組み込み関数の実装

まずは、組み込み関数を表現するための新しいクラスを作成します:

class BuiltinFun {
  constructor(name, func) {
    this.name = name;
    this.func = func;
  }
}

次に、evalProgram関数を以下のように修正して、組み込み関数を環境に追加します:

function evalProgram(program) {
  const env = {};
  const funEnv = {};
+ const builtinFunEnv = {
+   'print': new BuiltinFun('print', (args) => {
+     console.log(...args);
+     return args[0];
+   }),
+   'add': new BuiltinFun('add', (args) => args.reduce((a, b) => a + b, 0)),
+   'mul': new BuiltinFun('mul', (args) => args.reduce((a, b) => a * b, 1)),
+ };
  let result = null;
  program.defs.forEach((d) => {
    funEnv[d.name] = d;
  });
  program.expressions.forEach((e) => {
-   result = eval(e, env, funEnv);
+   result = eval(e, env, funEnv, builtinFunEnv);
  });
  return result;
}

ここでは、printaddmulという3つの組み込み関数を定義しています。名前からおおまかに挙動は推測できるかと思いますが、

  • print:引数を標準出力に出力する
  • add:引数を加算する
  • mul:引数を乗算する

となります。

次に、eval関数を修正して、組み込み関数を扱えるようにします:

- function eval(expr, env, funEnv) {
+ function eval(expr, env, funEnv, builtinFunEnv) {
    // ... 省略
    } else if (expr instanceof FunCall) {
-     const def = funEnv[expr.name];
-     if (!def) throw `function ${expr.name} is not defined`;
+     const def = funEnv[expr.name] || builtinFunEnv[expr.name];
+     if (!def) throw `function ${expr.name} is not defined`;
+
+     const args = expr.args.map((a) => eval(a, env, funEnv, builtinFunEnv));
+
+     if (def instanceof BuiltinFun) {
+       return def.func(args);
+     }

-     const args = expr.args.map((a) => eval(a, env, funEnv));

      const newEnv = {}
      for (let i = 0; i < def.args.length; i++) {
        newEnv[def.args[i]] = args[i];
      }
-     return eval(def.body, newEnv, funEnv);
+     return eval(def.body, newEnv, funEnv, builtinFunEnv);
    }
    // ... 省略
  }

これで、組み込み関数を呼び出せるようになりました。

組み込み関数の使用例

新しく実装した組み込み関数を使って、簡単なプログラムを書いてみましょう:

const program = new Program(
  // function print_add(x, y) {
  //   print(add(mul(x, y), 3)
  // }
  // print_add(5, 3);
  [
    new FunDef("print_add", ["x", "y"],
      new FunCall("print", 
        new FunCall("add", 
          new FunCall("mul", new VarRef("x"), new VarRef("y")),
          new Num(3)
        )
      )
    )
  ],
  new FunCall("print_add", new Num(5), new Num(3))
);

evalProgram(program);

このプログラムは以下のような動作をします:

  • 関数print_add(x, y)を定義する
    • add(mul(x, y), 3)を計算し、結果を表示する
  • print_add(5, 3)を呼び出す

実行結果は以下のようになります:

18

5 * 3 + 3は18ですから正しく計算できていることがわかります。また、これまでと違い結果をconsole.logで出力していませんが、組み込み関数printがその役割を果たしてくれています。

組み込み関数を実装することで、インタプリタの機能が大幅に向上し、より実用的なプログラムを書けるようになりました。

そろそろ機能が揃ってきたので、次回はプログラムの抽象構文木をそのまま渡すのではなく、JSONで渡せるようにしてみましょう。

nextbeat Tech Blog

Discussion