Skip to content

効果(Effects)

Onion は、呼び出しがプログラムの外の世界に何をしうるかを追跡します。すべてのメソッドは 効果集合 を持ち、それは何も実行せずに計算されます。--effects で表示できます:

$ onion --effects report.on
Report#readCfg(java.lang.String): read
Report#main(java.lang.String[]): console,read

このページは、語彙・事実の出どころ・解析が約束すること/しないことのリファレンスです。 この上に載る capability 層(tool 宣言、requires、--plan)は tools ガイドで解説しています。このデータ層が先に出荷され、 capability 層は今それを土台にして動いています。

語彙

効果は「外の世界への触れ方」に一つずつ名前を付けたものです。呼び出しはその集合を持ち、 空集合は pure と表示されます。

read はファイルシステムの読み取り、write は書き込みです。Files::copy のように両方を 行う操作は両方を持ちます。net は方向を問わないネットワーク通信です。exec はプロセスの 起動・置換・終了を指し、だから Proc::run だけでなく System::exit もこれを持ちます。 env はプロセス環境の読み取り(環境変数、システムプロパティ、作業ディレクトリ、JVM 統計) です。clock は時計を読むことと時計に対してブロックすることの両方を含むので、 DateTime::now と Timing::sleep が同じ効果を共有します。rand は乱数源からの取得、 console は接続された stdin / stdout / stderr の使用です。

9 つ目の unknown だけは種類が違います。これは表がこの呼び出しを保証できないことを 意味します。表に載っていない Java メソッドは「純粋だと分かっている」わけではなく、そう 装えばこの語彙の上に築かれる保証すべてが嘘になります。だから unknown は他の効果と同じ ように伝播する効果であり、capability 層はこれを「明示的な許可が要る」と扱います。 「無害」とは決して扱いません。

事実の出どころ

このコンパイラでは効果をアノテーションに載せられません — Java メソッドのアノテーションは 型検査器に届かず、バイトコードにも出力されず、最適化器はメソッドを再構築するときに捨てて しまいます。だから事実は帯域外、コンパイラに同梱されるリソース(onion/effect-table.txt) にあります。各行が 1 メソッド(またはワイルドカードでクラス全体)を分類し、個別エントリは クラスのワイルドカードに勝ちます。ほぼ純粋なクラスが例外だけ申告する書き方です:

onion.DateTime#*=pure
onion.DateTime#now=clock
onion.Files#copy=read,write
onion.Cli#parse=console,exec

表には標準ライブラリの効果を持つ全表面 — Files、FileResource、Proc、Http、 HttpResource、Config、IO、Cli、DateTime、Timing、Rand、OnionMath、 Archive、Db、Net、Server、Future、Concurrent、ToolCli — の 分類に加えて、純粋な残りの stdlib(Proc の結果、Http のレスポンス、Json::value のように 入口の関数が返す入れ子の値も含む)、よく使う JDK の値型・コレクション型と java.time (now だけは clock)の純粋ベースライン、JDK のファイル I/O(java.io のファイルストリームと File、java.nio.file.Files。BufferedReader などのラッパーは System.in やソケットを包むこともあるため unknown のまま)、JDK の既知の効果点(System::getenv、System::exit、Runtime::exec、Thread::sleep、 System::out の型としての PrintStream)が入っています。

粒度は型単位であり、インスタンス単位ではありません。ファイルの上の PrintStream は本来 write ですが、Onion コードで PrintStream といえば System::out / System::err なので、 この型は console に分類されています。この区別が重要な場面では、stdlib 自身の入り口 (Files、IO)が正確な経路です。

ライブラリは自分の呼び出しを分類できる

ライブラリの jar は、自分の呼び出しを unknown のままにしておく必要はありません。 コンパイル時クラスパス上の jar —— -classpath、スクリプトの //> using dep が 解決した jar、プロジェクトの [dependencies] —— は、組み込みの表と同じ形式の表を

META-INF/onion/effect-table.txt

に同梱でき、コンパイラはそのコンパイルに限ってそれを組み込みの表に合流させます。 そのライブラリを呼ぶ tool は、unknown ではなく本当の効果(requires { net, env })を 宣言できます。

表のどの行でも、効果に固定オペランド effect:operand を付けられます:

# Class#method=effect[:operand][,effect[:operand]...]   または   Class#method=pure
onion.llm.Llm#ask=net:api.anthropic.com,env:ANTHROPIC_API_KEY
onion.llm.Llm#text=pure

オペランドは最初の : の後ろから次の , までの全部(前後の空白は除く)です。空には できず、pure と unknown には付けられません。これまでどおり、素の net は「オペランド なし」です。固定オペランドは、そのメソッドのすべての呼び出しについて、リテラル引数から 読み取ったオペランドとまったく同じように --plan に表示され、--contract の staticOperands に入ります(tool を参照)。 それらに追加されるのであって、置き換えるのではありません。ですから Llm::ask(...) を 呼ぶ tool の plan は net api.anthropic.com と env ANTHROPIC_API_KEY になります。 組み込みの表もこの構文を使えます。

ライブラリの表が従う規則:

  • 自分のクラスだけ。 行が有効なのは、そのクラスのファイル(a.b.C$D なら a/b/C$D.class)が同じ jar に入っているときだけです。それ以外のクラスを名指す行は 無視され、jar ごとに1つの警告 W0018 が jar・行・クラス名を示します。
  • onion.* と java.* では組み込みの表が勝つ。 どちらかの名前空間のクラスでは、 組み込みのエントリ(個別でもワイルドカードでも)がライブラリのものに勝ちます。組み込みの 表が扱っていない onion.* のクラス(onion.llm.Llm など)はライブラリが分類できます。
  • クラスパスで先の jar がクラスを持つ。 2つの jar が同じクラスを分類しているときは、 クラスパス上で先にある jar —— JVM がそのクラスを読み込む jar —— の行が使われ、もう一方の そのクラスの行は参照されません。
  • 壊れた表は丸ごと無視される。 解析できない行が1つでもあれば表全体が無効になり (1行だけ捨てると、そのメソッドをもっと緩いワイルドカードが支配しかねないため)、jar と 行を示す W0017 として報告されます。その jar への呼び出しは再び unknown になるので、 tool は unknown を認めていなければ E0077 で失敗します。

ライブラリの表が読まれるのは、プログラムが tool を宣言しているとき(と --effects) だけです。各 jar の解析結果はパス・サイズ・更新時刻でキャッシュされるので、常駐デーモンと 言語サーバーが jar に払うコストは1回きりです。クラスパス上のディレクトリは参照しません。 どちらの警告も --Wno W0017,W0018 で抑止できます。

メソッドの集合はどう計算されるか

プログラム内で定義されたメソッドについては、コンパイラが型付きのボディを走査し、中の すべての呼び出しについて和集合を取ります。外部呼び出しは表の判定を、プログラム内の他の メソッドへの呼び出しはそのメソッド自身の推論結果を、推移的に合流させます。再帰・相互再帰は 固定点で収束します。コンストラクタも同じように参加します — new Loud() は Loud の コンストラクタが行うこと(スーパークラス連鎖を含む)を運びます。

2 つの規則は意図的な過大近似で、最大の精度よりも境界検査に対する健全性を選んでいます。 クロージャのボディは、それを最終的に呼び出すメソッドではなく生成するメソッドに課金 されます(関数値の呼び出し自体は表で pure)。生成箇所こそ検査器が常に見える場所だから です。そして表もコンパイル対象ソースも説明できない呼び出しは、無言で落とされるのではなく unknown を寄与します。

結果はもう一方の方向にも過大近似です。解析が報告するのはボディがなしうることであり、 ある実行で実際にすることではありません。一度も実行されない分岐も効果を寄与します。 これはこの語彙が答えるべき問い —「これを実行するとき、私は何を許可するのか」— に対して 正しい方向です。効果の見落としは穴になりますが、余分に載るのは単に保守的なだけです。

約束しないこと

リフレクション、ClassLoader の細工、ネイティブコードはモデルの外です。それらに到達する 呼び出しの箇所で unknown として現れ、その先を透視することはありません。表は出荷された stdlib を分類したものであり、onion.Files という名前のユーザー提供クラスをソースと突き 合わせることはしません。そして効果集合はセキュリティサンドボックスではありません。何も プログラムの実行を止めません。集合は「実行してよいか」を決める上位の層への、正直な入力 です。

同じ走査は --plan のために、ソースから読み取れるオペランド —— リテラル URL のホスト、 Proc のコマンドとそのリテラル引数、System::getenv の変数名、リテラルのパス —— も記録します (ソースに書かれたオペランド を参照)。 これは効果集合とは逆向きの下界です。並んだオペランドはどれもプログラムが実際に到達しうる ものですが、リテラルでないオペランドは推測されず (operand not statically known) と 報告されます。