libperl-macrogen 0.1.3

Generate Rust FFI bindings from C macro functions in Perl headers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
//! Pipeline API for libperl-macrogen
//!
//! 3フェーズ構成の Pipeline アーキテクチャを提供:
//! 1. Preprocess: C ヘッダーファイルのプリプロセス
//! 2. Infer: マクロと inline 関数の型推論
//! 3. Generate: Rust コード生成
//!
//! # 使用例
//!
//! ```ignore
//! use libperl_macrogen::Pipeline;
//!
//! // 一括実行
//! let mut output = File::create("macro_fns.rs")?;
//! Pipeline::builder("wrapper.h")
//!     .with_auto_perl_config()?
//!     .with_codegen_defaults()
//!     .with_bindings("bindings.rs")
//!     .build()?
//!     .generate(&mut output)?;
//!
//! // 段階的実行
//! let preprocessed = Pipeline::builder("wrapper.h")
//!     .with_auto_perl_config()?
//!     .with_codegen_defaults()
//!     .build()?
//!     .preprocess()?;
//!
//! let inferred = preprocessed
//!     .with_bindings("bindings.rs")
//!     .infer()?;
//!
//! let generated = inferred
//!     .with_strict_rustfmt()
//!     .generate(&mut output)?;
//! ```

use std::collections::HashMap;
use std::io::Write;
use std::path::PathBuf;

use crate::perl_config::{get_perl_config, PerlConfigError, get_default_target_dir};
use crate::preprocessor::{PPConfig, Preprocessor};
use crate::rust_codegen::{BindingsInfo, CodegenConfig as RustCodegenConfig, CodegenDriver, CodegenStats};
use crate::infer_api::{InferResult, InferError};
use crate::error::EnrichedCompileError;

// ============================================================================
// Error types
// ============================================================================

/// Pipeline 実行時のエラー
#[derive(Debug)]
pub enum PipelineError {
    /// Perl 設定取得エラー
    PerlConfig(PerlConfigError),
    /// プリプロセス/パースエラー(ファイルパスと該当行で強化済み)
    Compile(EnrichedCompileError),
    /// 推論エラー
    Infer(InferError),
    /// I/O エラー
    Io(std::io::Error),
}

impl std::fmt::Display for PipelineError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PipelineError::PerlConfig(e) => write!(f, "Perl config error: {}", e),
            PipelineError::Compile(e) => write!(f, "Compile error: {}", e),
            PipelineError::Infer(e) => write!(f, "Inference error: {}", e),
            PipelineError::Io(e) => write!(f, "I/O error: {}", e),
        }
    }
}

impl std::error::Error for PipelineError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            PipelineError::PerlConfig(e) => Some(e),
            PipelineError::Compile(e) => Some(e),
            PipelineError::Infer(e) => Some(e),
            PipelineError::Io(e) => Some(e),
        }
    }
}

impl From<PerlConfigError> for PipelineError {
    fn from(e: PerlConfigError) -> Self {
        PipelineError::PerlConfig(e)
    }
}

impl From<EnrichedCompileError> for PipelineError {
    fn from(e: EnrichedCompileError) -> Self {
        PipelineError::Compile(e)
    }
}

impl From<InferError> for PipelineError {
    fn from(e: InferError) -> Self {
        PipelineError::Infer(e)
    }
}

impl From<std::io::Error> for PipelineError {
    fn from(e: std::io::Error) -> Self {
        PipelineError::Io(e)
    }
}

// ============================================================================
// Phase-specific Config structs
// ============================================================================

/// Preprocessor フェーズの設定
#[derive(Debug, Clone)]
pub struct PreprocessConfig {
    /// 入力ファイル
    pub input_file: PathBuf,
    /// インクルードパス (-I)
    pub include_paths: Vec<PathBuf>,
    /// プリプロセッサ定義 (-D)
    pub defines: HashMap<String, Option<String>>,
    /// ターゲットディレクトリ(Perl CORE)
    pub target_dir: Option<PathBuf>,
    /// マクロ展開マーカーを出力
    pub emit_markers: bool,
    /// ラップ対象マクロ(inline関数内で特別扱いするマクロ)
    pub wrapped_macros: Vec<String>,
    /// PERLVAR/PERLVARI/PERLVARA/PERLVARIC 呼び出しを観測して
    /// `PerlvarDict` に集めるかどうか。
    ///
    /// デフォルト `true`(opt-out)。生成される `macro_bindings.rs` の
    /// 末尾に `PL_xxx!()` 宣言マクロのセクションを追加する。
    /// 必要なければ `with_perlvar_collection(false)` で無効化できる。
    pub collect_perlvars: bool,
    /// デバッグ出力
    pub debug_pp: bool,
}

impl PreprocessConfig {
    /// 入力ファイルのみを指定した最小構成
    pub fn new(input_file: impl Into<PathBuf>) -> Self {
        Self {
            input_file: input_file.into(),
            include_paths: Vec::new(),
            defines: HashMap::new(),
            target_dir: None,
            emit_markers: false,
            wrapped_macros: Vec::new(),
            collect_perlvars: true,
            debug_pp: false,
        }
    }

    /// PPConfig に変換
    pub(crate) fn to_pp_config(&self) -> PPConfig {
        PPConfig {
            include_paths: self.include_paths.clone(),
            predefined: self.defines.iter()
                .map(|(k, v)| (k.clone(), v.clone()))
                .collect(),
            debug_pp: self.debug_pp,
            target_dir: self.target_dir.clone(),
            emit_markers: self.emit_markers,
        }
    }
}

/// Inference フェーズの設定
#[derive(Debug, Clone, Default)]
pub struct InferConfig {
    /// Rust バインディングファイル
    pub bindings_path: Option<PathBuf>,
    /// apidoc ファイルパス(省略時は自動検索)
    pub apidoc_path: Option<PathBuf>,
    /// apidoc ディレクトリ
    pub apidoc_dir: Option<PathBuf>,
    /// apidoc マージ後にダンプして終了
    pub dump_apidoc_after_merge: Option<String>,
    /// 型推論デバッグ対象のマクロ名リスト
    pub debug_type_inference: Vec<String>,
    /// codegen をスキップしたい関数名リストファイル。
    /// 1 行 1 名、`#` コメント可。複数指定可。
    /// JSON の apidoc patches (`skip_codegen`) にマージされる
    /// (同名は既存が優先)。
    pub skip_codegen_lists: Vec<PathBuf>,
    /// 対象 perl の build mode(threaded / non-threaded)
    ///
    /// `None` の場合は `auto-detect`(実行時に `perl Config{usethreads}` を読む)。
    /// `Some(...)` で明示指定(テスト用)。
    pub perl_build_mode: Option<crate::perl_config::PerlBuildMode>,
}

impl InferConfig {
    pub fn new() -> Self {
        Self::default()
    }
}

/// Codegen フェーズの設定
#[derive(Debug, Clone)]
pub struct CodegenConfig {
    /// Rust edition for rustfmt
    pub rust_edition: String,
    /// rustfmt 失敗時にエラー
    pub strict_rustfmt: bool,
    /// マクロ定義位置コメント
    pub macro_comments: bool,
    /// inline 関数を出力
    pub emit_inline_fns: bool,
    /// マクロを出力
    pub emit_macros: bool,
    /// ヘッダーに出力する use 文(空ならデフォルト)
    pub use_statements: Vec<String>,
    /// AST ダンプ対象関数名(デバッグ用)
    pub dump_ast_for: Option<String>,
    /// 型推論ダンプ対象関数名(デバッグ用)
    pub dump_types_for: Option<String>,
}

impl Default for CodegenConfig {
    fn default() -> Self {
        Self {
            rust_edition: "2024".to_string(),
            strict_rustfmt: false,
            macro_comments: false,
            emit_inline_fns: true,
            emit_macros: true,
            use_statements: Vec::new(),
            dump_ast_for: None,
            dump_types_for: None,
        }
    }
}

impl CodegenConfig {
    /// RustCodegenConfig に変換
    pub(crate) fn to_rust_codegen_config(&self) -> RustCodegenConfig {
        RustCodegenConfig {
            emit_inline_fns: self.emit_inline_fns,
            emit_macros: self.emit_macros,
            include_source_location: self.macro_comments,
            use_statements: self.use_statements.clone(),
            dump_ast_for: self.dump_ast_for.clone(),
            dump_types_for: self.dump_types_for.clone(),
        }
    }
}

// ============================================================================
// PipelineBuilder
// ============================================================================

/// Pipeline を構築するための Builder
#[derive(Debug)]
pub struct PipelineBuilder {
    preprocess: PreprocessConfig,
    infer: InferConfig,
    codegen: CodegenConfig,
}

impl PipelineBuilder {
    /// 入力ファイルを指定して Builder を作成
    pub fn new(input_file: impl Into<PathBuf>) -> Self {
        Self {
            preprocess: PreprocessConfig::new(input_file),
            infer: InferConfig::new(),
            codegen: CodegenConfig::default(),
        }
    }

    // === Preprocess 設定 ===

    /// Perl Config.pm から自動設定(--auto 相当)
    ///
    /// インクルードパス、プリプロセッサ定義、ターゲットディレクトリを
    /// Perl の Config.pm から取得して設定する。
    pub fn with_auto_perl_config(mut self) -> Result<Self, PipelineError> {
        let perl_cfg = get_perl_config()?;
        self.preprocess.include_paths = perl_cfg.include_paths;
        self.preprocess.defines = perl_cfg.defines.into_iter().collect();
        self.preprocess.target_dir = get_default_target_dir().ok();
        Ok(self)
    }

    /// コード生成に推奨される設定を適用
    ///
    /// 以下を設定:
    /// - wrapped_macros: ["assert", "assert_"] (inline関数内のassertを正しく変換)
    pub fn with_codegen_defaults(mut self) -> Self {
        self.preprocess.wrapped_macros = vec![
            "assert".to_string(),
            "assert_".to_string(),
        ];
        self
    }

    /// インクルードパスを追加 (-I)
    pub fn with_include(mut self, path: impl Into<PathBuf>) -> Self {
        self.preprocess.include_paths.push(path.into());
        self
    }

    /// マクロ定義を追加 (-D)
    pub fn with_define(mut self, name: impl Into<String>, value: Option<impl Into<String>>) -> Self {
        self.preprocess.defines.insert(name.into(), value.map(|v| v.into()));
        self
    }

    /// ターゲットディレクトリを設定
    pub fn with_target_dir(mut self, path: impl Into<PathBuf>) -> Self {
        self.preprocess.target_dir = Some(path.into());
        self
    }

    /// マクロ展開マーカーを出力
    pub fn with_emit_markers(mut self) -> Self {
        self.preprocess.emit_markers = true;
        self
    }

    /// PERLVAR/PERLVARI/PERLVARA/PERLVARIC 観測の有効/無効を指定。
    ///
    /// デフォルトは有効 (opt-out)。`false` を渡すと PERLVAR コレクションを
    /// 無効化し、`PL_xxx!()` セクションは出力されなくなる。
    pub fn with_perlvar_collection(mut self, enable: bool) -> Self {
        self.preprocess.collect_perlvars = enable;
        self
    }

    /// プリプロセッサデバッグ出力を有効化
    pub fn with_debug_pp(mut self) -> Self {
        self.preprocess.debug_pp = true;
        self
    }

    // === Infer 設定 ===

    /// Rust バインディングファイルを指定
    pub fn with_bindings(mut self, path: impl Into<PathBuf>) -> Self {
        self.infer.bindings_path = Some(path.into());
        self
    }

    /// apidoc ファイルを指定
    pub fn with_apidoc(mut self, path: impl Into<PathBuf>) -> Self {
        self.infer.apidoc_path = Some(path.into());
        self
    }

    /// apidoc ディレクトリを指定
    pub fn with_apidoc_dir(mut self, path: impl Into<PathBuf>) -> Self {
        self.infer.apidoc_dir = Some(path.into());
        self
    }

    /// apidoc マージ後にダンプして終了(デバッグ用)
    pub fn with_dump_apidoc(mut self, filter: impl Into<String>) -> Self {
        self.infer.dump_apidoc_after_merge = Some(filter.into());
        self
    }

    /// 型推論デバッグ対象のマクロを指定
    pub fn with_debug_type_inference(mut self, macros: Vec<String>) -> Self {
        self.infer.debug_type_inference = macros;
        self
    }

    /// codegen をスキップする関数名リストファイルを追加
    ///
    /// ファイル形式: 1 行 1 名、`#` コメント可、空行無視。
    /// 複数回呼び出してファイルを追加可能。JSON の apidoc patches
    /// (`skip_codegen`) と同時指定できる(同名は patches 優先)。
    pub fn with_skip_codegen_list(mut self, path: impl Into<PathBuf>) -> Self {
        self.infer.skip_codegen_lists.push(path.into());
        self
    }

    /// 対象 perl の build mode を明示指定する
    ///
    /// 省略時は実行時に `perl -V:usethreads` から auto-detect。
    /// テストやクロスコンパイル用途で固定したい場合のみ呼び出す。
    pub fn with_perl_build_mode(mut self, mode: crate::perl_config::PerlBuildMode) -> Self {
        self.infer.perl_build_mode = Some(mode);
        self
    }

    // === Codegen 設定 ===

    /// rustfmt 失敗時にエラー終了
    pub fn with_strict_rustfmt(mut self) -> Self {
        self.codegen.strict_rustfmt = true;
        self
    }

    /// Rust edition を指定
    pub fn with_rust_edition(mut self, edition: impl Into<String>) -> Self {
        self.codegen.rust_edition = edition.into();
        self
    }

    /// マクロ定義位置コメントを有効化
    pub fn with_macro_comments(mut self) -> Self {
        self.codegen.macro_comments = true;
        self
    }

    /// AST ダンプ対象関数名を指定(デバッグ用)
    pub fn with_dump_ast_for(mut self, name: impl Into<String>) -> Self {
        self.codegen.dump_ast_for = Some(name.into());
        self
    }

    /// 型推論ダンプ対象関数名を指定(デバッグ用)
    pub fn with_dump_types_for(mut self, name: impl Into<String>) -> Self {
        self.codegen.dump_types_for = Some(name.into());
        self
    }

    // === Build ===

    /// Pipeline を構築
    pub fn build(self) -> Result<Pipeline, PipelineError> {
        Ok(Pipeline {
            preprocess_config: self.preprocess,
            infer_config: self.infer,
            codegen_config: self.codegen,
        })
    }

    /// PreprocessConfig のみを取り出す(Preprocessor 単独使用時)
    pub fn preprocess_config(self) -> PreprocessConfig {
        self.preprocess
    }

    /// InferConfig を取り出す
    pub fn infer_config(&self) -> &InferConfig {
        &self.infer
    }

    /// CodegenConfig を取り出す
    pub fn codegen_config(&self) -> &CodegenConfig {
        &self.codegen
    }
}

// ============================================================================
// Pipeline (Initial state)
// ============================================================================

/// 初期状態の Pipeline
pub struct Pipeline {
    preprocess_config: PreprocessConfig,
    infer_config: InferConfig,
    codegen_config: CodegenConfig,
}

impl Pipeline {
    /// Builder を作成
    pub fn builder(input_file: impl Into<PathBuf>) -> PipelineBuilder {
        PipelineBuilder::new(input_file)
    }

    /// PreprocessConfig への参照を取得
    pub fn preprocess_config(&self) -> &PreprocessConfig {
        &self.preprocess_config
    }

    /// InferConfig への参照を取得
    pub fn infer_config(&self) -> &InferConfig {
        &self.infer_config
    }

    /// CodegenConfig への参照を取得
    pub fn codegen_config(&self) -> &CodegenConfig {
        &self.codegen_config
    }

    /// Phase 1: プリプロセスのみ実行
    pub fn preprocess(self) -> Result<PreprocessedPipeline, PipelineError> {
        // PPConfig を構築
        let pp_config = self.preprocess_config.to_pp_config();

        // Preprocessor を初期化
        let mut pp = Preprocessor::new(pp_config);

        // wrapped_macros を登録
        for macro_name in &self.preprocess_config.wrapped_macros {
            pp.add_wrapped_macro(macro_name);
        }

        // PERLVAR コレクション (opt-out、デフォルト有効)
        let perlvar_dict = if self.preprocess_config.collect_perlvars {
            let (dict, c_var, c_init, c_array, c_const) =
                crate::perlvar_dict::PerlvarCollector::new_set();
            // 借用衝突回避のため intern を先に済ませる
            let interner = pp.interner_mut();
            let id_var = interner.intern("PERLVAR");
            let id_init = interner.intern("PERLVARI");
            let id_array = interner.intern("PERLVARA");
            let id_const = interner.intern("PERLVARIC");
            pp.set_macro_called_callback(id_var, Box::new(c_var));
            pp.set_macro_called_callback(id_init, Box::new(c_init));
            pp.set_macro_called_callback(id_array, Box::new(c_array));
            pp.set_macro_called_callback(id_const, Box::new(c_const));
            Some(dict)
        } else {
            None
        };

        // ファイルを処理
        if let Err(e) = pp.add_source_file(&self.preprocess_config.input_file) {
            return Err(PipelineError::Compile(e.with_files(pp.files())));
        }

        Ok(PreprocessedPipeline {
            preprocessor: pp,
            infer_config: self.infer_config,
            codegen_config: self.codegen_config,
            perlvar_dict,
        })
    }

    /// Phase 2: 推論まで実行(プリプロセスも含む)
    pub fn infer(self) -> Result<InferredPipeline, PipelineError> {
        self.preprocess()?.infer()
    }

    /// Phase 3: コード生成まで実行(全フェーズ)
    pub fn generate<W: Write>(self, writer: W) -> Result<GeneratedPipeline, PipelineError> {
        self.infer()?.generate(writer)
    }
}

// ============================================================================
// PreprocessedPipeline
// ============================================================================

/// プリプロセス完了状態
pub struct PreprocessedPipeline {
    preprocessor: Preprocessor,
    infer_config: InferConfig,
    codegen_config: CodegenConfig,
    /// PERLVAR コレクション (有効時に Some)。
    /// `Rc<RefCell<...>>` 経由でコールバックと共有しているので、
    /// add_source_file 完了時点でコールバックが書き込み済み。
    perlvar_dict: Option<std::rc::Rc<std::cell::RefCell<crate::perlvar_dict::PerlvarDict>>>,
}

impl PreprocessedPipeline {
    /// Preprocessor への参照を取得
    pub fn preprocessor(&self) -> &Preprocessor {
        &self.preprocessor
    }

    /// Preprocessor への可変参照を取得
    pub fn preprocessor_mut(&mut self) -> &mut Preprocessor {
        &mut self.preprocessor
    }

    /// Preprocessor を消費して取得
    pub fn into_preprocessor(self) -> Preprocessor {
        self.preprocessor
    }

    /// InferConfig への参照を取得
    pub fn infer_config(&self) -> &InferConfig {
        &self.infer_config
    }

    /// CodegenConfig への参照を取得
    pub fn codegen_config(&self) -> &CodegenConfig {
        &self.codegen_config
    }

    // === Infer 設定を追加で指定可能 ===

    /// Rust バインディングファイルを指定
    pub fn with_bindings(mut self, path: impl Into<PathBuf>) -> Self {
        self.infer_config.bindings_path = Some(path.into());
        self
    }

    /// apidoc ファイルを指定
    pub fn with_apidoc(mut self, path: impl Into<PathBuf>) -> Self {
        self.infer_config.apidoc_path = Some(path.into());
        self
    }

    /// apidoc ディレクトリを指定
    pub fn with_apidoc_dir(mut self, path: impl Into<PathBuf>) -> Self {
        self.infer_config.apidoc_dir = Some(path.into());
        self
    }

    /// codegen をスキップする関数名リストファイルを追加
    pub fn with_skip_codegen_list(mut self, path: impl Into<PathBuf>) -> Self {
        self.infer_config.skip_codegen_lists.push(path.into());
        self
    }

    /// Phase 2: 推論を実行
    pub fn infer(self) -> Result<InferredPipeline, PipelineError> {
        use crate::apidoc::resolve_apidoc_path;
        use crate::infer_api::{run_inference_with_preprocessor, DebugOptions};

        // apidoc パスを解決
        let apidoc_path = resolve_apidoc_path(
            self.infer_config.apidoc_path.as_deref(),
            true, // auto_mode
            self.infer_config.apidoc_dir.as_deref(),
        ).map_err(|e| PipelineError::Infer(InferError::ApidocResolve(e)))?;

        // デバッグオプションを構築
        let has_debug_opts = self.infer_config.dump_apidoc_after_merge.is_some()
            || !self.infer_config.debug_type_inference.is_empty();
        let debug_opts = if has_debug_opts {
            Some(DebugOptions {
                dump_apidoc_after_merge: self.infer_config.dump_apidoc_after_merge.clone(),
                debug_type_inference: self.infer_config.debug_type_inference.clone(),
            })
        } else {
            None
        };

        // 推論を実行
        let result = run_inference_with_preprocessor(
            self.preprocessor,
            apidoc_path.as_deref(),
            self.infer_config.bindings_path.as_deref(),
            debug_opts.as_ref(),
            &self.infer_config.skip_codegen_lists,
            self.infer_config.perl_build_mode,
        )?;

        match result {
            Some(mut infer_result) => {
                // PERLVAR コレクションを取り出して結果に転送
                if let Some(rc) = self.perlvar_dict {
                    // コールバックとの共有 Rc。Preprocessor 内にコールバックが
                    // まだ生きている (= 参照カウント > 1) 想定なので、try_unwrap
                    // ではなく素直に clone で取り出す。
                    infer_result.perlvar_dict = rc.borrow().clone();
                }
                Ok(InferredPipeline {
                    result: infer_result,
                    codegen_config: self.codegen_config,
                })
            }
            None => {
                // デバッグダンプで早期終了
                // 空の結果を返すか、専用のエラーを返すか検討が必要
                // ここでは Io エラーとして扱う(暫定)
                Err(PipelineError::Io(std::io::Error::new(
                    std::io::ErrorKind::Interrupted,
                    "Debug dump caused early exit",
                )))
            }
        }
    }

    /// Phase 3: コード生成まで実行(推論も含む)
    pub fn generate<W: Write>(self, writer: W) -> Result<GeneratedPipeline, PipelineError> {
        self.infer()?.generate(writer)
    }
}

// ============================================================================
// InferredPipeline
// ============================================================================

/// 推論完了状態
pub struct InferredPipeline {
    result: InferResult,
    codegen_config: CodegenConfig,
}

impl InferredPipeline {
    /// InferResult への参照を取得
    pub fn result(&self) -> &InferResult {
        &self.result
    }

    /// InferResult を消費して取得
    pub fn into_result(self) -> InferResult {
        self.result
    }

    /// CodegenConfig への参照を取得
    pub fn codegen_config(&self) -> &CodegenConfig {
        &self.codegen_config
    }

    // === Codegen 設定を追加で指定可能 ===

    /// rustfmt 失敗時にエラー終了
    pub fn with_strict_rustfmt(mut self) -> Self {
        self.codegen_config.strict_rustfmt = true;
        self
    }

    /// Rust edition を指定
    pub fn with_rust_edition(mut self, edition: impl Into<String>) -> Self {
        self.codegen_config.rust_edition = edition.into();
        self
    }

    /// マクロ定義位置コメントを有効化
    pub fn with_macro_comments(mut self) -> Self {
        self.codegen_config.macro_comments = true;
        self
    }

    /// AST ダンプ対象関数名を指定(デバッグ用)
    pub fn with_dump_ast_for(mut self, name: impl Into<String>) -> Self {
        self.codegen_config.dump_ast_for = Some(name.into());
        self
    }

    /// 型推論ダンプ対象関数名を指定(デバッグ用)
    pub fn with_dump_types_for(mut self, name: impl Into<String>) -> Self {
        self.codegen_config.dump_types_for = Some(name.into());
        self
    }

    /// Phase 3: コード生成
    pub fn generate<W: Write>(self, mut writer: W) -> Result<GeneratedPipeline, PipelineError> {
        let rust_codegen_config = self.codegen_config.to_rust_codegen_config();

        let bindings_info = self.result.rust_decl_dict.as_ref()
            .map(|d| BindingsInfo::from_rust_decl_dict(d))
            .unwrap_or_default();

        let mut driver = CodegenDriver::new(
            &mut writer,
            self.result.preprocessor.interner(),
            &self.result.enum_dict,
            &self.result.infer_ctx,
            bindings_info,
            rust_codegen_config,
        );

        driver.generate(&self.result)?;

        let stats = driver.stats().clone();

        // PERLVAR section: emit at end of macro_bindings.rs.
        // Empty dict (e.g. when collect_perlvars=false) is a no-op.
        crate::perlvar_emitter::emit_perlvar_section(
            &mut writer,
            &self.result.perlvar_dict,
            self.result.perl_build_mode.is_threaded(),
        )?;

        // TODO: strict_rustfmt の処理
        // 現状は CodegenDriver が rustfmt を呼び出さないため、
        // ここで別途 rustfmt を実行する必要がある

        Ok(GeneratedPipeline {
            result: self.result,
            stats,
        })
    }
}

// ============================================================================
// GeneratedPipeline
// ============================================================================

/// コード生成完了状態
pub struct GeneratedPipeline {
    result: InferResult,
    /// コード生成の統計情報
    pub stats: CodegenStats,
}

impl GeneratedPipeline {
    /// 統計情報を取得
    pub fn stats(&self) -> &CodegenStats {
        &self.stats
    }

    /// InferResult への参照を取得
    pub fn result(&self) -> &InferResult {
        &self.result
    }

    /// InferResult を消費して取得
    pub fn into_result(self) -> InferResult {
        self.result
    }
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_pipeline_builder_basic() {
        let builder = PipelineBuilder::new("test.h")
            .with_include("/usr/include")
            .with_define("FOO", Some("1"))
            .with_bindings("bindings.rs");

        assert_eq!(builder.preprocess.input_file, PathBuf::from("test.h"));
        assert_eq!(builder.preprocess.include_paths.len(), 1);
        assert_eq!(builder.preprocess.defines.get("FOO"), Some(&Some("1".to_string())));
        assert_eq!(builder.infer.bindings_path, Some(PathBuf::from("bindings.rs")));
    }

    #[test]
    fn test_pipeline_builder_codegen_defaults() {
        let builder = PipelineBuilder::new("test.h")
            .with_codegen_defaults();

        assert_eq!(builder.preprocess.wrapped_macros, vec!["assert", "assert_"]);
    }

    #[test]
    fn test_preprocess_config_to_pp_config() {
        let mut config = PreprocessConfig::new("test.h");
        config.include_paths.push(PathBuf::from("/usr/include"));
        config.defines.insert("FOO".to_string(), Some("1".to_string()));
        config.debug_pp = true;

        let pp_config = config.to_pp_config();
        assert_eq!(pp_config.include_paths.len(), 1);
        assert_eq!(pp_config.predefined.len(), 1);
        assert!(pp_config.debug_pp);
    }

    #[test]
    fn test_codegen_config_default() {
        let config = CodegenConfig::default();
        assert_eq!(config.rust_edition, "2024");
        assert!(!config.strict_rustfmt);
        assert!(config.emit_inline_fns);
        assert!(config.emit_macros);
    }
}