【ITニュース解説】Tsonnet #23 - Mirror, mirror on the wall, who's the most self-referential of them all?
2025年09月10日に「Dev.to」が公開したITニュース「Tsonnet #23 - Mirror, mirror on the wall, who's the most self-referential of them all?」について初心者にもわかりやすく解説しています。
ITニュース概要
Tsonnetという言語で、オブジェクトが自身のプロパティを参照する`self.field`機能を実装した。当初は構文エラーだったが、記述を認識できるよう修正。さらに、オブジェクト外での`self`利用や、プロパティ間の無限ループを防ぐため、評価前の利用範囲チェックと循環参照検出機能を追加し、安全に自己参照できるようになった。
ITニュース解説
Tsonnetという、設定ファイルを記述するための言語(またはそれを処理するシステム)について、オブジェクトが自分自身の情報にアクセスできるようにする「自己参照」機能の実装とその課題解決について解説する。ソフトウェア開発において、設定ファイルは多くの場所で利用され、その設定が複雑になると、柔軟な記述方法が求められる。特に、一つの設定が他の設定に依存する場合、オブジェクト自身が自分のプロパティを参照できると非常に便利になる。
これまでTsonnetでは、{ one: 1, two: self.one + 1 }のように、オブジェクト内でselfというキーワードを使って自身の別のプロパティを参照することができなかった。この機能を実現するためには、Tsonnetがコードを理解するプロセスを段階的に修正する必要がある。
まず、コードの最小単位を認識する「レキサー」と、その最小単位から文法構造を組み立てる「パーサー」を更新する。レキサーには、selfという新しいキーワードと、プロパティにアクセスするための.(ドット)記号を認識するように教える。次に、パーサーはself.fieldという形が「オブジェクトのフィールドへのアクセス」を意味することを理解するように変更する。
これらの文法的な変更が認識された後、プログラムの構造を表す「抽象構文木(AST)」に、ObjectSelfとObjectFieldAccessという新しい要素(ノード)を追加する。ObjectSelfノードには、そのオブジェクトを唯一無二に識別するためのEnv.env_idというIDが付与される。これにより、複数のオブジェクトが存在する場合でも、どのselfがどのオブジェクトを指しているのかを正確に追跡できるようになる。
最後に、実際にコードを解釈・実行する「インタープリタ」を修正する。インタープリタがオブジェクトの定義を処理する際、そのオブジェクトのIDを生成し、selfという名前でそのIDを「環境」に追加する。こうすることで、self.fieldのような記述に出会ったときに、環境から現在のオブジェクトIDを見つけ出し、そのIDを持つオブジェクトの指定されたフィールドの値を取得できるようになる。この仕組みにより、オブジェクトは自身のプロパティを正しく参照できるようになる。
自己参照の実装後、selfをオブジェクトの外部で使った場合に問題が発生した。例えば、local _two = self.one + 1;のように、オブジェクトの定義の外でselfを使うと、Jsonnetではエラーとなるべきだが、Tsonnetではエラーにならず、代わりに誤った値が出力されてしまう現象が確認された。
この問題の根本原因は、Tsonnetが「遅延評価(lazy evaluation)」という方式を採用していることにある。遅延評価とは、値が必要になるまで計算を遅らせる方式のことだ。オブジェクトの外部でselfが使われていても、その_twoという変数が実際に使われるまで評価が保留される。その結果、selfがオブジェクトの内部で定義されているかどうかのチェックが後回しになり、不適切な場所でのselfの使用が見過ごされてしまうのだ。つまり、selfが評価される時点では、すでにその変数があるべき「スコープ」(有効範囲)から外れてしまっている可能性がある。
この遅延評価による問題を解決するため、Tsonnetの処理フローに「スコープ検証(scope checking)」という新しい段階が追加された。スコープ検証は、実際の評価が始まる前に抽象構文木(AST)を「先行して」走査し、selfのような特殊な識別子が適切なコンテキスト(文脈)で使われているかを検査する。
具体的には、スコープ検証モジュールは、in_object(現在オブジェクトの内部にいるかどうか)やobject_depth(オブジェクトの入れ子の深さ)といった情報を含む「コンテキスト」を管理する。ASTを再帰的に巡回していく中で、オブジェクトの定義に入るとin_objectをtrueに設定し、オブジェクトのスコープを出るとfalseに戻す。
もし、オブジェクトの外部でselfキーワードやself.fieldのようなオブジェクトフィールドアクセスが見つかった場合、スコープ検証はその場でエラーを報告する。これにより、遅延評価の特性に起因するバグを防ぎ、selfの不適切な使用を評価が始まる前の早い段階で検出できるようになる。この新しい処理段階は、システム全体をより堅牢にするために非常に重要だ。
自己参照機能が実装されたことで、新たな問題として「循環参照(cyclic reference)」が発生する可能性が出てきた。これは、オブジェクトのフィールドが互いに参照し合うことで無限ループに陥る状況を指す。例えば、{ a: self.b, b: self.a }というオブジェクトを考えてみる。ここでaの値を評価しようとするとself.bが必要になり、bの値を評価しようとするとself.aが必要になる。このようにお互いを無限に参照し合うため、決して値が決定せず、プログラムは停止してしまう。
Jsonnetのような既存の言語では、このような循環参照が発生した場合、実行時にシステムのスタック(処理履歴を記録するメモリ領域)が上限を超えてしまい、「最大スタックフレーム数超過」というエラーとして報告される。しかし、このエラーメッセージは非常に長く、どこで問題が起きているのかが分かりにくい場合が多い。理想的には、このような循環参照はプログラムの実行前、つまり「コンパイル時」に検出されるべきだ。
Tsonnetでは、この循環参照の問題をコンパイル時に検出できるように改善された。既存の「型チェッカー」に、オブジェクトフィールド間の循環参照を検出するロジックが追加された。
具体的には、型チェッカーがオブジェクトを処理する際、まずselfを環境に追加し、そのオブジェクトのローカル変数やフィールドを型変換する。その上で、各オブジェクトフィールドについて循環参照のチェックを行う。self.fieldのような参照に出会うと、selfが指すオブジェクトのIDとアクセスしているフィールド名を組み合わせて一意の識別子を生成し、この識別子がすでに評価中のリストに含まれていないかを確認する。もし含まれていれば、それは循環参照であると判断し、エラーとして報告する。
この改良により、{ a: self.b, b: self.a }のような直接的な循環だけでなく、local a = self.b, b: a,のようにローカル変数とオブジェクトフィールドが絡み合う複雑な循環参照も、実行時ではなく型チェックの段階で早期に検出できるようになる。これにより、開発者はより明確なエラーメッセージを受け取り、問題を迅速に特定・修正できるようになった。
Tsonnetにおいて、オブジェクトが自分自身のプロパティを参照できる「自己参照」機能が安全かつ正確に実装された。この機能の実現には、レキサーとパーサーの拡張、抽象構文木への新ノード追加、そしてインタープリタでの環境管理の変更が必要だった。さらに、遅延評価に起因するselfの不適切なスコープでの使用を防ぐため、評価前に抽象構文木を走査する「スコープ検証」の段階が導入された。また、オブジェクトフィールド間での無限ループを引き起こす「循環参照」も、実行時ではなくコンパイル時に検出できるよう、既存の型チェッカーのロジックが拡張された。これらの改善により、Tsonnetはより堅牢で信頼性の高い設定言語としての機能を提供できるようになった。今後は、エラーメッセージの改善など、さらなる機能強化が予定されている。