RINEARNでは先日、オープンソースの式計算ライブラリ「Exevalator(エグゼバレータ)」の最新版 Ver.2.0.0 をリリースいたしました。
2.0 のリリースと言っても、今回のアップデートは、1件のバグ修正のみを含むもので、ソースコード上の改修規模はごくわずか(数行)です。
ただ、その修正対象のバグが、バグなのか仕様なのか分かり辛かったものであり、恐ら仕様と思われて使われていたケースも十分考えられるものでした。 そのため、今回は単なるバグ修正ではなく、「互換性を壊す仕様変更」の側面も帯びている事を踏まえて、念のためメジャーバージョン番号を 1 から 2 へ上げた、という状況です。
本記事では、その概要を解説いたします。
まず最初に、Exevalator(エグゼバレータ)について簡単に解説いたします。
Exevalator は、アプリやソフトウェアに部品として組み込んで使う「ライブラリ」の一つで、ユーザーが入力した計算式の値を算出したりするのに使えます。
例えば何らかのアプリを開発していて、ユーザーがそのアプリ上で
や
のような計算式を自由に入力し、その値を求めたい、という場面はしばしば生じます。 ただ、こういった処理は、一見単純に思えるのですが、実は作るのが結構ややこしくて難しく、なかなか大変です。
そこで Exevalator は、こういった処理をライブラリ(部品プログラム)として提供する事で、手軽に実現できるようにしたものです。 Java/C++/C#/Rust の4つの言語に対応しており、1〜2枚のソースコードを、プロジェクトのソースコードフォルダに放り込むだけですぐに使えます。
Exevalator についてのより詳しい情報は、以下の公式サイトをご参照ください:
ここからは、本題のバグの内容について解説いたします。
Exevalator は、アプリのユーザー側が関数などを自作する事はできないのですが、アプリの開発者側が関数を用意して、Exevalator に組み込む事ができます(以下、組み込み関数)。 組み込まれた関数は、ユーザーが計算式の中で使えるようになります。
例えば、Exevalator に関数 fun(a, b, c) を組み込むと、ユーザーは計算式の中で、以下のように関数 fun を呼び出して使う事ができるようになります:
この関数 fun は、アプリの開発言語(Java/C++/C#/Rust のどれか)で実装し、Exevalarotに登録して組み込みます。 例えば Java 言語の場合は、関数は以下のように実装します:
上のクラス MyFunction (名前はなんでも可)をインスタンス化して Exevalator に登録します。その際に、式内で呼び出す際の関数名 "fun" を指定します。
すると、先ほどのように式内で関数呼び出し fun(123, 456, 789) を行った際に、上記の MyFunction の invoke メソッドが呼び出され、同メソッドの配列引数 args に、式内での関数呼び出しの実引数 123, 456, 789 が渡されます(詳細は次節)。 その引数の値に基づいて、invoke メソッド内で計算などを行い、結果を戻り値として返すと、それが式内での fun(123, 456, 789) の値として使用されます。
今回問題となったのは、上記のような計算式内での関数呼び出しの実引数「 123, 456, 789 」が、invoke メソッドの配列引数 args に格納される際の、要素の順序です。
直観的には、以下のような順序で格納される事を誰もが期待するはずです:
実際、今回のリリース(Ver.2.0)以降では、引数は上記の順序で格納されて渡されます。 そのように挙動を修正した、というのが今回のアップデートです。
では、以前の Ver.1.0.x ではどうなっていたかというと、以下のように、逆順で格納されていました:
これは、何か意図があってのものではなく、式の構文解析を行うパーサの実装の不備※によるものでした。
※ 各引数(一般に部分式になり得る)の構文木ノードは、左から順に解析された上でスタックに積まれますが、それを単純に取り出すと、詰んだ順とは逆の順序(LIFO順)になります。 従って、取り出し後に順序反転をして、積む際の順序に戻す(FIFO順にする)必要があるのですが、そのままになっていました。
Exevalator の構文解析処理(パーサ)は、Java製スクリプトエンジン Vnano のものをシンプル化しつつ他言語移植したものですが、 その移植の際に、順序反転の行が抜け落ちてしまったのが原因でした。
しかしながら args 配列の要素の順序に関しては、特にドキュメント内での記載が無く(バグが無ければごく自然な順序になっているはずであったため)、 そのため この挙動がバグなのか仕様なのかが分かり辛い状況だった かもしれません。 実際、今回のバグが見つかった経緯は、使用を試されている方から、バグか仕様かのお問合せをいただいた事によるものでした。
従って、このバグによる順序を「そういう仕様」と思って使用されているケースも、恐らく一定数存在するはずという前提で対応する必要があります。 そのようなケースでは、この順序を正しい形に戻す事は、互換性を壊す仕様変更とほぼ同じ側面を持っています。
ふつう、バージョンを表す Ver.1.0.x のようなコードにおいて、「x」のような末尾の番号は、上がっても互換性が大きく壊れないような小さなアップデートの番号を表す事が多いです(少なくとも RINEARN 製のソフトではそうしています)。 従って、今回のバグ修正において、Ver.1.0.0 → Ver.1.0.1 のように末尾の番号を上げるだけだと、上記の「仕様と判断して使われている」というケースでかなりの混乱を招いてしまいます。
そのため、今回のバグ修正はわずか数行のものでしたが、安全のため先頭の数字を上げて、Ver.2.0.0 としてリリースした次第です。
Exevalator のソースコードリポジトリ 内には、テストコードも同梱されており、push 時に自動でテストが走るようになっています。 その中で、関数については以下のような呼び出しパターンがテストされていました:
上記の通り、結構ねじった呼び出しパターンのテストが存在しますが、今回のバグは、これらを全て通過(合格)してしまっていました。
一見すると、引数の評価順序が逆になっていたら、上記のテストは絶対に通らないような気がします。 しかしなぜ通ってしまっていたかを調べると、「テストで用いていた関数 funC が、引数の和を返すものになっていた」というのが原因でした。 複数の値の和は、値の順序を入れ替えても同じ結果になるためです。
そのため、今回はテストコードに、「複数の引数の順序と値が、共に正しい事をテストする関数 funD 」を用いたものを追加しました。
このバグで非常に恥ずかしい点は、お問合せいただくまで公式側で見逃してしまっていた事です。 自動テストを通過していても、実際に組み込み関数を実装して使えば、すぐに分かったはずです。
これについては、「公式側での Exevalator の実用頻度が少なすぎた」という所に根本原因があります。
そもそも Exevalator は、先行してJava言語で開発したスクリプトエンジン「 Vnano 」の処理系が当初想定よりも大きくなってしまったため、 機能量を絞ってコンパクト化しつつ、複数言語(Java/C++/C#/Rust)に移植したものでした(詳細はこちら)。
しかし、RINEARNで何か開発する際はJava言語を採用する事が多く、その場合の式計算などでは、やはり機能量が多い Vnano の方を使用してきました(次期版リニアングラフ3Dでもそうです)。 そのため、Exevalator は開発してリリースしたものの、公式側としては実用場面がほぼ無いという状態が続いていました。 これが、今回のような大きなバグが長期で残り続けた一番の原因だと考えています。
これについての根本的な対策は、やはり どんなライブラリでも、公式側として最低何か 1 つでも実用アプリケーションを作って使い倒す という事が重要なのだと思いました。
従って、RINEARN でも今後、何かしらの Exevalator を用いたアプリかコマンドラインツールなどを製作してリリースしたいと考えています。 まだ内容は未定ですが、一つ考えているのは、「 Windows / Linux / macOS で使える、RustかC++製の式計算コマンド 」などです。
今回のバグ修正についての詳細は、以上の通りです。
今回のバグにより、混乱をお掛けしてしまった方々には、深くお詫び申し上げます。 引数順序が逆になっているとは、本当に初歩的なパーサ実装のミスなのですが、まさか上記のテストを全て通過しているのにそんな事が起こっているとは全く思わず、本当に申し訳ありません…
また、今回のバグについてお問合せをいただいたユーザー様には、この場をお借りして心より感謝申し上げます。
今後も、Exevalator に関する情報は、このお知らせコーナーで逐次お知らせしていきます。