Rust製パーサコンビネータ「nom」のアーキテクチャと基礎概念

IResult型:入力と出力の明示的結合

nom の中心となる抽象は、すべてのパーサー関数が返す IResult<I, O> です。これは内部で以下の型エイリアスとして定義されています。

pub type IResult<I, O> = Result<(I, O), Err<I>>;

この構造は、パースプロセスを純粋な関数合成に適合させるために設計されています。

  • I(Input):現在のカーソル位置から残っている未解析の入力データ(典型的には &str&[u8] のスライス)。
  • O(Output):成功時に抽出された構造化データや値。
  • Err<I>nom::Err 列挙体で、解析が中断された理由と発生時の入力行番号・バイト位置を格納します。

成功時には残り入力量と共に結果タプルが返されるため、複数のパーサーを連鎖させても状態管理の手間が軽減されます。

コンビネータによる関数レベルのパース構築

単一機能のパーサーを組み合わせて複雑な文法を表現するパターンです。主要な演算子は以下の通り分類できます。

  • 分岐選択 (alt):引数リスト内のパーサーを上から順に試行し、最初の成功した結果を返します。バックトラックを用いた並列マッチングを実現します。
  • 結果変換 (map, and_then):指定した関数を適用して O 型の値を操作します。文字列から数値型への変換など、中間データの加工によく使用されます。
  • 前置/後置抽出 (preceded, terminated):特定のマークアップ(開始タグや終了括弧)を確認した後、対象部分のみを抽出して discard します。
  • 区切り内抽出 (delimited)F(G(H)) の形に対応し、左右の境界パターンを消費して中央部分の結果を返します。XMLタグやJSONオブジェクトのような閉じ込み構造に向いています。
  • 区切り付きペア (separated_pair)(左パーサー, 区切りパーサー, 右パーサー) を受け取り、中間の区切り文字を保持しつつ両端のデータをタプル化します。
  • 繰返し・オプショナル (many0, many1, count, opt):配列やリスト構文、必須/任意パラメータの扱いを宣言的に記述するための高階関数群です。

プリミティブ・パーサーの実装

外部依存のない低水準なマッチング関数が提供されており、これらを土台にして高度なロジックを構築します。

  • tag:固定長の文字列またはバイトシーケンスに完全一致するマーカー。
  • alpha1 / digit1:アルファベットまたは数字の連続ブロック。空文字列を許可しません。
  • char:単一文字または UTF-8 マルチバイト文字の判定。
  • is_not / take_while:指定文字セットを含まない範囲、または条件関数が true を返す範囲のスライス抽出。
  • space0 / space1:空白類(スペース、タブ、改行)の任意出現および必須出現を処理します。

エラーモデルと制御フロー

nom のエラー体系は、失敗の種類を明確に区別することでデバッグ性とパフォーマンスを両立しています。

  • Err::Error:フォールバック可能な一時的不一致。他の代替パスを試行する際に利用されます。
  • Err::Failure:致命的なバリデーション違反。即座に処理を中断し上位へ伝播します。
  • Err::Incomplete:ストリーミングモードで使用され、データが完全に読み込まれていないことを示す非同期パース用フラグ。

デフォルトでは逐次結合時に自動バックトラックを行わない設計ですが、alt や明示的なリワインド処理を実装することで柔軟なフォールバック制御が可能です。

メモリ効率と実行性能

本ライブラリの最大の特徴はゼロコピーアーキテクチャです。入力の一部分だけを取り出す場合でもヒープ割り当てや文字列のコピーを行わず、元のバッファへの参照(スライス)だけを返却します。これにより GC パッシングやメモリ帯域の圧迫を回避でき、ネットワークプロトコルのバイナリエンディアン解析や大規模テキストログのフィルタリングといったレイテンシ敏感なユースケースで顕著な速度差を発揮します。

実践的な実装例

以下は、カスタム設定フォーマットからセクション名とキー値の対を取得する例です。組み合わせによって条件付随型ロジックを構築しています。

use nom::{
    bytes::complete::tag,
    character::complete::alphanumeric1,
    sequence::tuple,
    IResult,
};

/// 「[セクション名] キー = 値」形式のパターンを分解する関数
pub fn extract_section_entry(input: &str) -> IResult&str, ((&str, &str), &str)> {
    tuple((
        tag("["),                           // セクション開始ブレース
        alphanumeric1,                      // セクション識別子
        tag("]"),                           // セクション終了ブレース
        tag("="),                           // キーバリュー連結子
        alphanumeric1                       // 設定値
    ))(input)
}

fn main() {
    let raw_config = "[auth] token=A1B2C3D4";
    
    match extract_section_entry(raw_config) {
        Ok((remaining, ((sec, _), val))) => {
            println!("セクション: {}", sec);
            println!("検出された値: {}", val);
            println!("残余ストリーム: '{}'", remaining);
        }
        Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => {
            eprintln!("スキーマ違反が発生しました: {:?}", e);
        }
        Err(nom::Err::Incomplete(_)) => {
            println!("入力データが断片的のため待機状態へ移行");
        }
    }
}

この例では tuple コンビネータを使用して多次元タプルを生成し、各フェーズで消費されるバイト列を一元管理しています。IResult から得られた残余入力は次のステージへのパイプライン投入にそのまま再利用可能です。

タグ: nom rust-lang parser-combinator zero-copy-allocation functional-parsing

6月5日 23:27 投稿