Skip to content

snix-evalを使ってRustで式を評価する

Published: at 12:00 AMSuggest Changes

はじめに

Nixを使ったアプリケーション開発において、式を評価するためには通常、外部プロセスとしてnix-instantiatenix evalなどのコマンドを実行するアプローチが取られてきた。しかし、この方法ではプロセス間通信のオーバーヘッドやエラーハンドリングの複雑さなど、いくつかの課題がある。理想的には、アプリケーション内で直接式を評価できれば、これらの問題を解決しつつ、より統合された開発体験を提供できる。

この記事では、Nixの新しいRust実装であるSnixのコンポーネント「snix-eval」を使用して、外部コマンドに依存せずにRustアプリケーション内から直接Nixの式を評価する方法を検証する。

Tvixとは

Tvix は、従来のC++で実装されたNixをRustで再実装するプロジェクトである。注目に値する点としてモジュール式のアーキテクチャを採用していることが挙げられる。つまり、様々なコンポーネントが独立して利用できるよう設計されていることで、ユースケースに応じて再利用したり置き換えたりできる柔軟性を備えている。

なお、Tvixは現在、Nix 2.3(flakesが実装される前のバージョン)との互換性を目指して開発されており、必ずしも現在のNixのユースケースを全て置き換えるものではない。

また、注目すべき動きとして、devenvが2024年10月にdevenv is switching its Nix implementation to Tvixというブログ記事を公開している。記事によると、devenvはNixの実装としてTvixを採用する方針を明らかにしており、その理由としてRustによるメモリ安全性とモジュール式のアーキテクチャによる独立して使用可能なコンポーネントの利点を挙げている。このような実用的なツールでのTvixの採用検討は、その実装の信頼性と可能性を示す重要な事例といえるだろう。

Snixとは

Snixは2025年3月に発表された、Tvixからフォークされたプロジェクトである。Tvixの設計思想や主要なコンポーネントを継承しつつも、より焦点を絞った開発アプローチを採用している。

SnixがTvixからフォークされた経緯は次のアナウンスに詳しいが、主な理由はTVLコミュニティとTvix開発者間における優先事項と方向性についての意見の相違である。また、大半がTvixとは無関係の巨大なモノレポの管理問題や両者間の異なるCI要件が新規貢献者のオンボーディングを困難にしていたことも挙げられる。これらの問題を解決し、Nixの革新的な実装に特化したコミュニティとインフラを整えるために、Snixとしてフォークするに至ったようだ。

このフォークにより、Snixは独自の発展を遂げつつあり、Nixエコシステムに新たな可能性をもたらしている。

今回の検証について

今回私が検証したのは、現在開発中のRustアプリケーションでNixの評価機能を組み込むためにSnixのsnix-evalコンポーネントを使用する方法である。このコンポーネントを使えば、外部のnixコマンドに依存せずに、アプリケーション内から式を直接評価できる可能性がある。

結論から言うと、基本的な式の評価はできるものの、nixpkgsのような複雑なケースでは制約があるため、現時点での採用は見送ることにした。以下にその詳細を説明する。

インストール方法

snix-evalはRustのcrateとして提供されているが、インストール時に注意が必要である。単にcargo add snix-evalを実行すると、v0.0.0の空のモジュールがダウンロードされてしまう。正しく導入するには、git経由でインストールする必要がある。

また、環境変数NIX_PATHを参照する機能を使用するにはimpureフィーチャーを有効にする必要がある。以下のようにCargo.tomlに追加する。

[package]
name = "snix-eval-example"
version = "0.1.0"
edition = "2024"

[dependencies]
snix-eval = { git = "https://git.snix.dev/snix/snix.git", rev = "853754d25fd44687ec893073f14db9f44185f36e", features = [ "impure" ] }

基本的な使い方

ライブラリの詳細は公式ドキュメントに記載されているが、以下に簡単な例を示す。まずは単純な数値計算の式の評価例から見てみよう。

簡単な四則演算

以下の例ではsnix-evalでどのようにNixの式をRust内で評価するかのサンプルを示している。

use snix_eval::Evaluation;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let builder = Evaluation::builder_pure()
        .mode(snix_eval::EvalMode::Lazy);
    let evaluation = builder.build();
    let result = evaluation.evaluate("1 + 2", None);
    
    if let Some(value) = result.value {
        println!("result: {:?}", value);
    } else if !result.errors.is_empty() {
        println!("error: {:?}", result.errors);
    }
    Ok(())
}

cargo run で実行すると result: Integer(3) と出力され、動作することが確認できた。このように、シンプルな式の評価はストレートに実装できる。

より複雑な評価:関数適用、ファイルの読み込みなど

基本的な評価は一通りできるようなので、以下のような関数の評価を行ってみる。 この関数では、builtin関数の呼び出し、with expressionによる短縮表記、変数束縛、関数適用といった機能が正しく動作することを確認できる。

default.nix

{ }:
with builtins;
let
  x = foldl' (acc: elem: acc + elem) 0 [
    1
    2
    3
  ];
in
x

上記のファイルを読み込むために前節のRustコードを書き換えた。 ここではさらに nix path の動作を確認するために sample=./. を追加した。

builderがファイルを読み込めるようにするためには snix_eval::EvalIO を実装したトレイトを渡しておく必要がある点に注意。

その他builderに渡すことのできるオプションは以下のドキュメントから確認することができる。 https://snix.dev/rustdoc/snix_eval/struct.EvaluationBuilder.html

src/main.rs

use snix_eval::{Evaluation, EvalIO};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let nix_path = "sample=./.";
    let builder = Evaluation::builder(Box::new(snix_eval::StdIO {}) as Box<dyn EvalIO>)
        .mode(snix_eval::EvalMode::Lazy)
        .enable_import()
        .nix_path(Some(nix_path.to_string()));
    
    let evaluation = builder.build();
    let result = evaluation.evaluate(r#"import <sample> { }"#, None);
    
    if let Some(value) = result.value {
        println!("result: {:?}", value);
    } else if !result.errors.is_empty() {
        println!("error: {:?}", result.errors);
    }

    Ok(())
}

これを実行すると result: Integer(6) と出力され、正しく式が評価されていることがわかる。

また、Nixファイルを以下のように変更しても問題なく評価できる。

      ];
    in
--- x
+++ rec {
+++   inherit x;
+++   y = x;
+++ }

Nixが遅延評価を行う言語であることを考慮すると、AttrSet の内部は実際に呼び出されるまで計算が行われないことがわかる。 実際、snix-evalでも遅延評価モード(Lazy)では内部表現のまま出力される。 このままでは正確に評価できているかどうか判別するのが難しいため、正格評価を行うことで最終的な値を確認することとする。 つまり .mode(snix_eval::EvalMode::Lazy).mode(snix_eval::EvalMode::Strict) に変更すれば良い。 この場合の出力は次のようになり、再帰的な AttrSet もきちんと処理できていることがわかった。

result: Attrs(NixAttrs(Map({"x": Thunk(Thunk(RefCell { value: Evaluated(Integer(6)) })), "y": Thunk(Thunk(RefCell { value: Evaluated(Integer(6)) }))})))

この結果から、snix-evalがNixの複雑なデータ構造や評価モデルを忠実に再現していることが確認できる。遅延評価と正格評価の両方をサポートしていることも、実用的なアプリケーションでの使用において重要な特性である。

現状の制約と課題

ここまで snix-eval の動作を確認してみたが、実際には使用にあたりいくつかの重要な制約と課題が存在する。まず機能の問題として、一部のbuiltin関数が現時点では未実装であり利用できないことが挙げられる。例えばbuiltins.readFileを使用しようとすると、AttributeNotFoundエラーが発生する。このようなコア機能の欠如は、実用的なコードの多くが使用する基本操作に制限をかけることになる。また、エラーが発生した際のメッセージは下に示すように長大で入れ子状になっており、デバッグが難しい場合がある。

error: [Error { kind: BytecodeError(Error { kind: NativeError { gen_type: "force", err: Error { kind: BytecodeError(Error { kind: NativeError { gen_type: "force", err: Error { kind: AttributeNotFound { name: "readFile" }, span: Span { low: Pos(10), high: Pos(18) }, contexts: [], source: SourceCode(RefCell { value: CodeMap { files: [File("[code]")] } }) } }, span: Span { low: Pos(1), high: Pos(18) }, contexts: [], source: SourceCode(RefCell { value: CodeMap { files: [File("[code]")] } }) }), span: Span { low: Pos(1), high: Pos(22) }, contexts: [], source: SourceCode(RefCell { value: CodeMap { files: [File("[code]")] } }) } }, span: Span { low: Pos(1), high: Pos(22) }, contexts: [], source: SourceCode(RefCell { value: CodeMap { files: [File("[code]")] } }) }), span: Span { low: Pos(1), high: Pos(22) }, contexts: [], source: SourceCode(RefCell { value: CodeMap { files: [File("[code]")] } }) }]

このようなエラーメッセージは、内部実装の詳細が多く含まれており、一般的なユーザーにとって理解しづらい。エラーの原因と解決策を直感的に把握できるような、より洗練されたエラーハンドリングが望まれる。

関連して、複雑なコードの評価に関する制限も大きな課題である。特にnixpkgsのような大規模なコードベースの評価は現時点では不可能である。実際にimport <nixpkgs> { }を評価しようとすると、複雑なエラーチェーンが発生する。nixpkgsはNixエコシステムの根幹であり、これが評価できないことには、実用的なアプリケーションの実現は到底不可能である。

さらにSnixがflakesに対応しないことによる課題もある。 flakesはexperimentalでありながら既にデファクトスタンダードとして扱われていることが多い。そのため、気づかずflakesの機能に依存していることもある。 今回遭遇した問題だと、NixOSでnixpkgs.flake.setNixPath = trueを設定している場合にセットされるnix pathの問題があった。 このオプションを有効化すると環境変数NIX_PATHnixpkgs=flake:nixpkgsがセットされるのだが、Snixはflakesを解さないため、この環境変数を上書きするなどの対応が必要となる。

これらの制約を総合すると、現時点ではsnix-evalは単純な式の評価には十分使えるものの、本格的なアプリケーション開発での利用にはまだ機能不足であると言わざるを得ない。ただし、Snixの開発は活発に進行中であり、今後のバージョンでこれらの制約が徐々に解消されていくことが期待される。

まとめ

本記事では、snix-evalを用いてRustアプリケーション内からNixの式を評価する可能性を検証した。単純な数値計算や基本的な関数適用などは問題なく動作し、外部プロセスを呼び出さずに評価できることが確認できた。一方で、builtins.readFileなどの重要な関数の未実装や、nixpkgsのような複雑なコードの評価ができないなど、現時点では実用化に向けていくつかの障壁が存在している。

Snixのモジュール式設計は、Webブラウザでも動作するsnixboltのような多様なツールを可能にし、Rust製評価器の組み込みによって高速で統合された開発体験を実現する可能性を秘めている。cachixのような企業サポートやdevenvへの採用が進むにつれ、Snixの成熟度は着実に向上しており、現在の制約が徐々に解消されていくことが期待される。

本記事の検証は実用化にはまだ距離があることを示しているが、このような新しいアプローチがNixエコシステム全体に与える影響と、次世代の開発ツールへの可能性を引き続き注目していきたい。


Next Post
2024年を振り返る