【ITニュース解説】Where Four Backends Stop Agreeing
2026年09月07日に「Dev.to」が公開したITニュース「Where Four Backends Stop Agreeing」について初心者にもわかりやすく解説しています。
ITニュース概要
「lm」言語は、CやWebAssemblyなど4つの方式でプログラムを生成する。すべての方式で同じ結果が出れば正しいとするが、ゼロ除算や数値変換など、一部の動作は環境ごとに異なる。この異なる動作のリストは、プログラムの正しい振る舞いを保証し、開発コストの判断も示す重要な設計文書である。
ITニュース解説
プログラミング言語「lm」は、私たちが普段使うソフトウェアが裏側でどのように動いているか、そしてその「正しさ」をどのように保証しているかについて、興味深いアプローチをとっている。この言語は、C言語のコード、WebAssembly(ウェブブラウザで高速に動くバイナリ形式)、ARM64(スマートフォンなどに使われるCPUアーキテクチャ)、そして独自の仮想マシン(VM)用のバイトコードという、全く異なる四つの形式にコンパイルされる。これらの出力形式を「バックエンド」と呼ぶが、lm言語には、これら全てのバックエンドに共通する中間表現というものは存在しない。つまり、言語のソースコードがまず「抽象構文木(AST)」と呼ばれる木構造のデータに変換され、そこから各バックエンドが独立して、それぞれ独自の機械語やバイトコードを生成する仕組みになっているのだ。
この言語の「正しさ」の定義は非常にユニークで、革新的と言える。それは、「同じソースコードをコンパイルした時、四つのバックエンド全てが、バイト単位で全く同じ出力結果を生成すれば、そのプログラムは正しい」というものだ。例えば、私たちが書いたプログラムが「こんにちは」と出力するものであれば、C言語でコンパイルしたものも、WebAssemblyでコンパイルしたものも、ARM64とVMのバイトコードでコンパイルしたものも、全てが寸分違わず「こんにちは」と出力するべき、という厳密なルールだ。三つの実装が一致するだけでも十分な根拠となるが、さらに、VM用のバイトコードが他の三つとは全く異なる仕組みを持つインタープリタ(コードを一行ずつ実行するプログラム)で動くため、この四者一致は、プログラムの信頼性をより強固なものにしている。
しかし、このような厳格な「正しさ」の定義を掲げるプロジェクトにとって、最も重要な文書が一つ存在する。それは、「合意が保証されない場所のリスト」だ。これは、lm言語の仕様が明確に規定しておらず、代わりに各バックエンドのターゲット環境(C言語のコンパイラや、ARM64のCPUなど)が持つ独自の「セマンティクス」、つまり挙動が前面に出てしまう箇所をまとめたものだ。言い換えれば、言語側で「こう動くべき」と指示されていないため、個々のコンパイラや実行環境がそれぞれの都合で勝手に(しかし、それぞれの仕様に従って)処理してしまう部分なのである。このリストは、プログラムの動作を理解し、予期せぬ不一致を避ける上で不可欠な存在となっている。
具体的に、現在リストアップされている不一致の箇所は四つある。一つ目は「ゼロ除算」の扱いだ。C言語とARM64では、ゼロで数値を割った際に特にチェックされず、プログラムが予期しない動作をしたり、静かにクラッシュしたりする可能性がある。一方、WebAssemblyでは「トラップ」と呼ばれる処理中断が起こり、VMでは「スロー」という例外が発生して、プログラムが明示的にエラーを通知する。
二つ目は「範囲外の浮動小数点数から整数への型変換」だ。例えば、非常に大きな浮動小数点数を小さな整数型に変換しようとした場合、C言語ではその挙動は「未定義」とされており、コンパイラや実行環境によって結果が大きく異なる。ARM64では値が「飽和」し、変換先の型の最大値または最小値に丸められる。WebAssemblyではゼロ除算と同様に「トラップ」が発生し、VMでは「ラップ」と呼ばれる、桁あふれによって値が一周して循環する挙動を示す。同じ変換処理でも、四つのバックエンド全てで異なる結果になるのだ。
三つ目は「メモリ範囲外アクセス」だ。プログラムが確保されていないメモリ領域にアクセスしようとした場合、C言語とARM64では何も警告せず、プログラムが突然終了したり、不正なデータが読み込まれたりする。WebAssemblyでは「トラップ」が発生し、VMでは「スロー」が起こる。lm言語自体はメモリの境界チェックを行わず、メモリ割り当て(alloc)も失敗しないため、この問題はバックエンドの挙動に大きく依存する。
四つ目は「f64(倍精度浮動小数点数)の出力」だ。これはC言語の標準関数%.6fとJavaScriptのtoFixed(6)の結果が一致することに依存している。通常の値では問題ないが、無限大(Infinity)や非数(NaN)といった特殊な浮動小数点数を出力しようとした場合、それぞれの関数の実装によっては出力が一致しない可能性がある。
なぜこのような「不一致リスト」が必要なのか。もしこのリストがなければ、lm言語の「四者一致」をチェックするシステム、いわゆる「オラクル」は、誰かがゼロ除算などの特殊なケースを含むテストを書いた途端に、エラーメッセージで溢れかえってしまうだろう。たくさんのエラーは「ノイズ」となり、開発者はそのチェック結果を信頼しなくなり、やがては読まなくなってしまう。しかし、このリストが存在することで、オラクルは本当に予期せぬバグだけを検出し、その有効性を保つことができる。リストに記載された四つのケースは、いずれも言語の保証範囲内で書かれたプログラムでは到達しない、定義済み挙動の「エッジケース」であり、それら以外の全てがバイト単位で一致することこそが、lm言語の信頼性の「本当の信号」となるのだ。
このリストは単なるバグ報告ではない。それは「隠れた設計ドキュメント」としての役割も担っている。リストの各項目は、プロジェクトチームが「この機能の実装にはコストがかかりすぎるから見送る」と決断した内容を示しているのだ。例えば、全てのメモリアクセスに対して境界チェックを行うことや、ゼロ除算で必ずトラップさせること、範囲外の型変換の挙動を厳密に定義することなどは、いずれも言語の内部ループで多くの命令コストを必要とする。このようなコストを払って実装するよりも、それを「不一致点」として明確に書き出す方が、開発コストは安価であり、何よりも利用者に対して正直なアプローチと言える。
この四つの不一致点の中で、特にシステムエンジニアを目指す初心者には「浮動小数点数から整数への型変換」について注意を促したい。他の不一致点は、プログラムのクラッシュや明らかに意味不明な値(ゴミ値)を生成するため、すぐに問題に気づきやすい。しかし、浮動小数点数から整数へのキャストの場合、特にARM64の飽和挙動やVMのラップ挙動は、「もっともらしい、しかし間違った数値」を生成する可能性がある。例えば、非常に大きな浮動小数点数1e300を64ビット整数に変換しようとした場合、ARM64は最大値を返し、VMはラップされた値を返すかもしれない。これらはどちらも「数値」であり、プログラムはこれらの値を使って計算を続けてしまう可能性がある。その結果、本来とは異なる、しかし一見すると正しいような結果が導き出されてしまい、バグの発見が非常に困難になるのだ。
このlm言語の事例から得られる一般化された教訓は、複数の環境でプログラムの動作を保証する「差分テスト」という手法において、最も難しいのは「例外リスト」を作成することだということだ。多くの人が同じ入力を複数の実装に通すこと自体を難しいと考えるが、本当の難しさは、それぞれの実装が異なる挙動をする可能性のある場所を洗い出し、それをリスト化することにある。そしてこの例外リストは、単なるオーバーヘッドではなく、言語が本来なら明示すべきだった「仕様書」そのものである。二つのバックエンドが不一致を起こすまで発見されなかった、言語リファレンスに書かれるべき内容が、このリストには詰まっている。
このようなオラクルを構築する際には、例外リストの作成を「後から発見された厄介な問題」と捉えるのではなく、最初から「リスト作成のための予算」を確保すべきだ。このリストの長さは、開発中の言語の挙動がどれだけ明確に定義され、固められているかを示す公正な尺度となる。lm言語は比較的小規模な言語であるため、リストが四つと短い。しかし、より複雑な言語であれば、このリストはさらに長くなるだろう。
この「合意が保証されない場所のリスト」は、プロジェクトのREADME(説明文書)に、言語の保証内容と並んで明記されている。これは、言語のユーザーが、この言語のどの部分が厳密に保証され、どの部分が環境に依存するのかを明確に理解できるようにするための、誠実な取り組みの証拠だ。システムエンジニアを目指す上で、このような言語設計の思想や、動作の「正しさ」を追求する手法は、システムの信頼性を確保するために非常に重要な視点となる。