Skip to main content

provable_contracts/
build_helper.rs

1//! Build script helper for consuming crates.
2//!
3//! Consuming crates (realizar, aprender, trueno, entrenar) use this module
4//! in their `build.rs` to:
5//!
6//! 1. Read `binding.yaml` and extract all implemented bindings
7//! 2. Set `CONTRACT_<NAME>_<EQ>=bound` env vars for each binding
8//! 3. Fail the build if any binding has status `not_implemented`
9//!
10//! ## Usage in build.rs
11//!
12//! ```rust,ignore
13//! // build.rs
14//! fn main() {
15//!     provable_contracts::build_helper::verify_bindings(
16//!         "../provable-contracts/contracts/aprender/binding.yaml",
17//!         BindingPolicy::AllImplemented,
18//!     );
19//! }
20//! ```
21
22use std::path::Path;
23
24use crate::binding::{BindingRegistry, ImplStatus};
25
26/// Policy for handling unimplemented bindings.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum BindingPolicy {
29    /// All bindings must have status `implemented`. Any `partial` or
30    /// `not_implemented` binding is a compile error.
31    AllImplemented,
32
33    /// Only `implemented` bindings get env vars. `partial` and
34    /// `not_implemented` are warnings (printed to cargo stderr).
35    WarnOnGaps,
36
37    /// Tiered: `not_implemented` is an error, `partial` is a warning.
38    TieredEnforcement,
39}
40
41/// Result of binding verification.
42#[derive(Debug)]
43pub struct VerifyResult {
44    /// Number of bindings that got env vars set.
45    pub bound_count: usize,
46    /// Number of partial bindings (warnings).
47    pub partial_count: usize,
48    /// Number of not-implemented bindings (errors or warnings depending on policy).
49    pub not_implemented_count: usize,
50}
51
52/// Read `binding.yaml` and set `CONTRACT_*` env vars for `#[contract]` macros.
53///
54/// Call this from your `build.rs`. It:
55/// 1. Parses the binding YAML
56/// 2. For each `implemented` binding, emits `cargo:rustc-env=CONTRACT_<KEY>=bound`
57/// 3. Enforces the given policy for gaps
58///
59/// # Panics
60///
61/// Panics (failing the build) if:
62/// - The binding YAML cannot be read or parsed
63/// - Policy is `AllImplemented` and any binding is not `implemented`
64/// - Policy is `TieredEnforcement` and any binding is `not_implemented`
65#[allow(clippy::too_many_lines)]
66pub fn verify_bindings(binding_yaml_path: &str, policy: BindingPolicy) -> VerifyResult {
67    let path = Path::new(binding_yaml_path);
68
69    // Rerun build.rs if binding.yaml changes
70    println!("cargo:rerun-if-changed={binding_yaml_path}");
71
72    // Also rerun if the contracts directory changes
73    if let Some(parent) = path.parent() {
74        if let Some(grandparent) = parent.parent() {
75            println!("cargo:rerun-if-changed={}", grandparent.display());
76        }
77    }
78
79    let yaml_content = std::fs::read_to_string(path).unwrap_or_else(|e| {
80        panic!(
81            "CONTRACT BUILD ERROR: Cannot read binding YAML at '{}': {e}\n\
82             Hint: Ensure provable-contracts is checked out as a sibling directory.",
83            path.display()
84        );
85    });
86
87    let registry: BindingRegistry = serde_yaml::from_str(&yaml_content).unwrap_or_else(|e| {
88        panic!(
89            "CONTRACT BUILD ERROR: Cannot parse binding YAML at '{}': {e}",
90            path.display()
91        );
92    });
93
94    let mut result = VerifyResult {
95        bound_count: 0,
96        partial_count: 0,
97        not_implemented_count: 0,
98    };
99
100    for binding in &registry.bindings {
101        let env_key = make_env_key(&binding.contract, &binding.equation);
102
103        match binding.status {
104            ImplStatus::Implemented => {
105                println!("cargo:rustc-env={env_key}=bound");
106                result.bound_count += 1;
107            }
108            ImplStatus::Partial => {
109                result.partial_count += 1;
110                match policy {
111                    BindingPolicy::AllImplemented => {
112                        panic!(
113                            "CONTRACT BUILD ERROR: Binding {}.{} has status 'partial'. \
114                             Policy requires all bindings to be 'implemented'.\n\
115                             Module: {}\n\
116                             See: unified-contract-by-design.md §10",
117                            binding.contract,
118                            binding.equation,
119                            binding.module_path.as_deref().unwrap_or("(unknown)"),
120                        );
121                    }
122                    BindingPolicy::WarnOnGaps | BindingPolicy::TieredEnforcement => {
123                        println!(
124                            "cargo:warning=CONTRACT: partial binding {}.{} ({})",
125                            binding.contract,
126                            binding.equation,
127                            binding.module_path.as_deref().unwrap_or("?"),
128                        );
129                        // Still set env var for partial — the function exists, just incomplete
130                        println!("cargo:rustc-env={env_key}=partial");
131                    }
132                }
133            }
134            ImplStatus::NotImplemented => {
135                result.not_implemented_count += 1;
136                match policy {
137                    BindingPolicy::AllImplemented | BindingPolicy::TieredEnforcement => {
138                        panic!(
139                            "CONTRACT BUILD ERROR: Binding {}.{} has status 'not_implemented'. \
140                             All bindings must be implemented.\n\
141                             Equation: {}\n\
142                             Target: {}\n\
143                             See: unified-contract-by-design.md §10",
144                            binding.contract,
145                            binding.equation,
146                            binding.equation,
147                            binding.module_path.as_deref().unwrap_or("(unassigned)"),
148                        );
149                    }
150                    BindingPolicy::WarnOnGaps => {
151                        println!(
152                            "cargo:warning=CONTRACT: not_implemented binding {}.{} ({})",
153                            binding.contract,
154                            binding.equation,
155                            binding.module_path.as_deref().unwrap_or("?"),
156                        );
157                    }
158                }
159            }
160            ImplStatus::Pending => {
161                result.not_implemented_count += 1;
162                println!(
163                    "cargo:warning=CONTRACT: pending binding {}.{} ({})",
164                    binding.contract,
165                    binding.equation,
166                    binding.module_path.as_deref().unwrap_or("?"),
167                );
168            }
169        }
170    }
171
172    println!(
173        "cargo:warning=CONTRACT: {}/{} bindings bound ({} partial, {} not_implemented)",
174        result.bound_count,
175        registry.bindings.len(),
176        result.partial_count,
177        result.not_implemented_count,
178    );
179
180    result
181}
182
183/// Verify that functions named in binding.yaml actually exist in the crate source.
184///
185/// Scans `src_dir` for `pub fn <name>` declarations and checks that every
186/// `function` field in the binding registry has a matching source function.
187/// Returns the names of missing functions. If the list is non-empty and
188/// `hard_fail` is true, panics to fail the build.
189///
190/// This closes the "ghost binding" gap where `status: implemented` passes
191/// build.rs but the actual function doesn't exist (renamed, deleted, typo).
192///
193/// # Example
194///
195/// ```rust,ignore
196/// // build.rs
197/// let missing = provable_contracts::build_helper::verify_source_functions(
198///     "../provable-contracts/contracts/aprender/binding.yaml",
199///     "src/",
200///     true, // hard fail
201/// );
202/// ```
203pub fn verify_source_functions(
204    binding_yaml_path: &str,
205    src_dir: &str,
206    hard_fail: bool,
207) -> Vec<String> {
208    let path = Path::new(binding_yaml_path);
209    let Ok(yaml_content) = std::fs::read_to_string(path) else {
210        println!("cargo:warning=verify_source_functions: cannot read {binding_yaml_path}");
211        return vec![];
212    };
213    let Ok(registry) = serde_yaml::from_str::<BindingRegistry>(&yaml_content) else {
214        println!("cargo:warning=verify_source_functions: cannot parse {binding_yaml_path}");
215        return vec![];
216    };
217
218    // Collect all function names from bindings
219    let mut expected_fns: std::collections::HashSet<String> = std::collections::HashSet::new();
220    for b in &registry.bindings {
221        if b.status != ImplStatus::Implemented {
222            continue;
223        }
224        if let Some(ref func) = b.function {
225            // Extract just the function name (after last ::)
226            let short = func.rsplit("::").next().unwrap_or(func);
227            expected_fns.insert(short.to_lowercase());
228        }
229    }
230
231    if expected_fns.is_empty() {
232        return vec![];
233    }
234
235    // Scan source files for pub fn declarations
236    let mut found_fns: std::collections::HashSet<String> = std::collections::HashSet::new();
237    let src = Path::new(src_dir);
238    if src.exists() {
239        scan_source_fns(src, &mut found_fns);
240    }
241    // Also check crates/ subdirectory
242    let crates_dir = Path::new("crates");
243    if crates_dir.exists() {
244        scan_source_fns(crates_dir, &mut found_fns);
245    }
246
247    let mut missing: Vec<String> = expected_fns
248        .iter()
249        .filter(|name| !found_fns.contains(name.as_str()))
250        .cloned()
251        .collect();
252    missing.sort();
253
254    if !missing.is_empty() {
255        let count = missing.len();
256        let sample: Vec<_> = missing.iter().take(10).collect();
257        let msg = format!(
258            "[contract] verify_source_functions: {count} bound function(s) not found in source: {}{}",
259            sample
260                .iter()
261                .map(|s| s.as_str())
262                .collect::<Vec<_>>()
263                .join(", "),
264            if count > 10 {
265                format!(" (and {} more)", count - 10)
266            } else {
267                String::new()
268            },
269        );
270
271        if hard_fail {
272            panic!("{msg}");
273        } else {
274            println!("cargo:warning={msg}");
275        }
276    }
277
278    missing
279}
280
281/// Recursively scan a directory for `pub fn` declarations.
282fn scan_source_fns(dir: &Path, found: &mut std::collections::HashSet<String>) {
283    let Ok(entries) = std::fs::read_dir(dir) else {
284        return;
285    };
286    for entry in entries.flatten() {
287        let path = entry.path();
288        if path.is_dir() {
289            let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
290            if name != "target" && name != ".git" {
291                scan_source_fns(&path, found);
292            }
293        } else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
294            if let Ok(content) = std::fs::read_to_string(&path) {
295                for line in content.lines() {
296                    let trimmed = line.trim();
297                    if trimmed.starts_with("pub fn ")
298                        || trimmed.starts_with("pub async fn ")
299                        || trimmed.starts_with("pub(crate) fn ")
300                    {
301                        let fn_part = trimmed
302                            .trim_start_matches("pub async fn ")
303                            .trim_start_matches("pub(crate) fn ")
304                            .trim_start_matches("pub fn ");
305                        let fn_name = fn_part
306                            .split('(')
307                            .next()
308                            .unwrap_or("")
309                            .split('<')
310                            .next()
311                            .unwrap_or("")
312                            .trim()
313                            .to_lowercase();
314                        if !fn_name.is_empty() {
315                            found.insert(fn_name);
316                        }
317                    }
318                }
319            }
320        }
321    }
322}
323
324/// Generate the env var key from contract name and equation name.
325///
326/// Same convention as `provable-contracts-macros::make_env_key`.
327fn make_env_key(contract: &str, equation: &str) -> String {
328    let contract_part = contract.to_uppercase().replace(['-', '.'], "_");
329    let equation_part = equation.to_uppercase().replace(['-', '.'], "_");
330    format!("CONTRACT_{contract_part}_{equation_part}")
331}
332
333#[cfg(test)]
334mod tests {
335    #![allow(clippy::all)]
336    use super::*;
337    /// Helper: write YAML content to a temp file and return the path string.
338    fn write_temp_yaml(content: &str) -> (tempfile::TempDir, String) {
339        let dir = tempfile::tempdir().expect("create tempdir");
340        let path = dir.path().join("binding.yaml");
341        std::fs::write(&path, content).expect("write yaml");
342        let path_str = path.to_str().unwrap().to_string();
343        (dir, path_str)
344    }
345
346    /// Minimal valid binding YAML with all-implemented bindings.
347    fn yaml_all_implemented() -> String {
348        r#"
349version: "1.0.0"
350target_crate: test_crate
351bindings:
352  - contract: softmax-v1.yaml
353    equation: softmax
354    module_path: "test::softmax"
355    function: my_softmax
356    status: implemented
357  - contract: relu-v1.yaml
358    equation: relu
359    module_path: "test::relu"
360    function: my_relu
361    status: implemented
362"#
363        .to_string()
364    }
365
366    /// Binding YAML with a partial binding.
367    fn yaml_with_partial() -> String {
368        r#"
369version: "1.0.0"
370target_crate: test_crate
371bindings:
372  - contract: softmax-v1.yaml
373    equation: softmax
374    module_path: "test::softmax"
375    function: my_softmax
376    status: implemented
377  - contract: relu-v1.yaml
378    equation: relu
379    module_path: "test::relu"
380    function: my_relu
381    status: partial
382"#
383        .to_string()
384    }
385
386    /// Binding YAML with a `not_implemented` binding.
387    fn yaml_with_not_implemented() -> String {
388        r#"
389version: "1.0.0"
390target_crate: test_crate
391bindings:
392  - contract: softmax-v1.yaml
393    equation: softmax
394    module_path: "test::softmax"
395    function: my_softmax
396    status: implemented
397  - contract: gelu-v1.yaml
398    equation: gelu
399    status: not_implemented
400"#
401        .to_string()
402    }
403
404    /// Binding YAML with a pending binding.
405    fn yaml_with_pending() -> String {
406        r#"
407version: "1.0.0"
408target_crate: test_crate
409bindings:
410  - contract: softmax-v1.yaml
411    equation: softmax
412    module_path: "test::softmax"
413    function: my_softmax
414    status: implemented
415  - contract: silu-v1.yaml
416    equation: silu
417    module_path: "test::silu"
418    status: pending
419"#
420        .to_string()
421    }
422
423    /// Binding YAML with mixed statuses (implemented + partial + `not_implemented` + pending).
424    fn yaml_mixed() -> String {
425        r#"
426version: "1.0.0"
427target_crate: test_crate
428bindings:
429  - contract: softmax-v1.yaml
430    equation: softmax
431    module_path: "test::softmax"
432    function: my_softmax
433    status: implemented
434  - contract: relu-v1.yaml
435    equation: relu
436    module_path: "test::relu"
437    function: my_relu
438    status: partial
439  - contract: gelu-v1.yaml
440    equation: gelu
441    status: not_implemented
442  - contract: silu-v1.yaml
443    equation: silu
444    status: pending
445"#
446        .to_string()
447    }
448
449    // ── make_env_key tests ──
450
451    #[test]
452    fn test_make_env_key_matches_macro_convention() {
453        assert_eq!(
454            make_env_key("rmsnorm-kernel-v1", "rmsnorm"),
455            "CONTRACT_RMSNORM_KERNEL_V1_RMSNORM"
456        );
457        assert_eq!(
458            make_env_key("gated-delta-net-v1", "decay"),
459            "CONTRACT_GATED_DELTA_NET_V1_DECAY"
460        );
461    }
462
463    #[test]
464    fn make_env_key_with_yaml_extension() {
465        assert_eq!(
466            make_env_key("softmax-kernel-v1.yaml", "softmax"),
467            "CONTRACT_SOFTMAX_KERNEL_V1_YAML_SOFTMAX"
468        );
469    }
470
471    #[test]
472    fn make_env_key_dots_replaced() {
473        assert_eq!(
474            make_env_key("my.contract.v1", "eq.1"),
475            "CONTRACT_MY_CONTRACT_V1_EQ_1"
476        );
477    }
478
479    // ── VerifyResult tests ──
480
481    #[test]
482    fn test_verify_result_defaults() {
483        let r = VerifyResult {
484            bound_count: 0,
485            partial_count: 0,
486            not_implemented_count: 0,
487        };
488        assert_eq!(r.bound_count, 0);
489        assert_eq!(r.partial_count, 0);
490        assert_eq!(r.not_implemented_count, 0);
491    }
492
493    #[test]
494    fn verify_result_debug_display() {
495        let r = VerifyResult {
496            bound_count: 5,
497            partial_count: 2,
498            not_implemented_count: 1,
499        };
500        let dbg = format!("{r:?}");
501        assert!(dbg.contains("bound_count: 5"));
502        assert!(dbg.contains("partial_count: 2"));
503        assert!(dbg.contains("not_implemented_count: 1"));
504    }
505
506    // ── BindingPolicy tests ──
507
508    #[test]
509    fn binding_policy_debug() {
510        assert_eq!(
511            format!("{:?}", BindingPolicy::AllImplemented),
512            "AllImplemented"
513        );
514        assert_eq!(
515            format!("{:?}", BindingPolicy::TieredEnforcement),
516            "TieredEnforcement"
517        );
518        assert_eq!(format!("{:?}", BindingPolicy::WarnOnGaps), "WarnOnGaps");
519    }
520
521    #[test]
522    fn binding_policy_clone_eq() {
523        let a = BindingPolicy::AllImplemented;
524        let b = a;
525        assert_eq!(a, b);
526
527        let c = BindingPolicy::WarnOnGaps;
528        assert_ne!(a, c);
529    }
530
531    // ── verify_bindings: success paths ──
532
533    #[test]
534    fn verify_bindings_all_implemented_all_ok() {
535        let (_dir, path) = write_temp_yaml(&yaml_all_implemented());
536        let result = verify_bindings(&path, BindingPolicy::AllImplemented);
537        assert_eq!(result.bound_count, 2);
538        assert_eq!(result.partial_count, 0);
539        assert_eq!(result.not_implemented_count, 0);
540    }
541
542    #[test]
543    fn verify_bindings_warn_on_gaps_all_implemented() {
544        let (_dir, path) = write_temp_yaml(&yaml_all_implemented());
545        let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
546        assert_eq!(result.bound_count, 2);
547        assert_eq!(result.partial_count, 0);
548        assert_eq!(result.not_implemented_count, 0);
549    }
550
551    #[test]
552    fn verify_bindings_tiered_all_implemented() {
553        let (_dir, path) = write_temp_yaml(&yaml_all_implemented());
554        let result = verify_bindings(&path, BindingPolicy::TieredEnforcement);
555        assert_eq!(result.bound_count, 2);
556        assert_eq!(result.partial_count, 0);
557        assert_eq!(result.not_implemented_count, 0);
558    }
559
560    // ── verify_bindings: WarnOnGaps with partial ──
561
562    #[test]
563    fn verify_bindings_warn_on_gaps_partial() {
564        let (_dir, path) = write_temp_yaml(&yaml_with_partial());
565        let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
566        assert_eq!(result.bound_count, 1);
567        assert_eq!(result.partial_count, 1);
568        assert_eq!(result.not_implemented_count, 0);
569    }
570
571    // ── verify_bindings: WarnOnGaps with not_implemented ──
572
573    #[test]
574    fn verify_bindings_warn_on_gaps_not_implemented() {
575        let (_dir, path) = write_temp_yaml(&yaml_with_not_implemented());
576        let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
577        assert_eq!(result.bound_count, 1);
578        assert_eq!(result.partial_count, 0);
579        assert_eq!(result.not_implemented_count, 1);
580    }
581
582    // ── verify_bindings: WarnOnGaps with mixed statuses ──
583
584    #[test]
585    fn verify_bindings_warn_on_gaps_mixed() {
586        let (_dir, path) = write_temp_yaml(&yaml_mixed());
587        let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
588        assert_eq!(result.bound_count, 1);
589        assert_eq!(result.partial_count, 1);
590        // not_implemented + pending both count as not_implemented_count
591        assert_eq!(result.not_implemented_count, 2);
592    }
593
594    // ── verify_bindings: TieredEnforcement with partial (warns, doesn't panic) ──
595
596    #[test]
597    fn verify_bindings_tiered_partial_warns_but_ok() {
598        let (_dir, path) = write_temp_yaml(&yaml_with_partial());
599        let result = verify_bindings(&path, BindingPolicy::TieredEnforcement);
600        assert_eq!(result.bound_count, 1);
601        assert_eq!(result.partial_count, 1);
602        assert_eq!(result.not_implemented_count, 0);
603    }
604
605    // ── verify_bindings: pending status handling ──
606
607    #[test]
608    fn verify_bindings_pending_status_warns() {
609        let (_dir, path) = write_temp_yaml(&yaml_with_pending());
610        let result = verify_bindings(&path, BindingPolicy::AllImplemented);
611        assert_eq!(result.bound_count, 1);
612        assert_eq!(result.not_implemented_count, 1); // pending counts here
613    }
614
615    #[test]
616    fn verify_bindings_pending_with_tiered() {
617        let (_dir, path) = write_temp_yaml(&yaml_with_pending());
618        let result = verify_bindings(&path, BindingPolicy::TieredEnforcement);
619        assert_eq!(result.bound_count, 1);
620        assert_eq!(result.not_implemented_count, 1);
621    }
622
623    #[test]
624    fn verify_bindings_pending_with_warn_on_gaps() {
625        let (_dir, path) = write_temp_yaml(&yaml_with_pending());
626        let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
627        assert_eq!(result.bound_count, 1);
628        assert_eq!(result.not_implemented_count, 1);
629    }
630
631    // ── verify_bindings: panic paths ──
632
633    #[test]
634    #[should_panic(expected = "CONTRACT BUILD ERROR")]
635    fn verify_bindings_all_implemented_panics_on_partial() {
636        let (_dir, path) = write_temp_yaml(&yaml_with_partial());
637        verify_bindings(&path, BindingPolicy::AllImplemented);
638    }
639
640    #[test]
641    #[should_panic(expected = "CONTRACT BUILD ERROR")]
642    fn verify_bindings_all_implemented_panics_on_not_implemented() {
643        let (_dir, path) = write_temp_yaml(&yaml_with_not_implemented());
644        verify_bindings(&path, BindingPolicy::AllImplemented);
645    }
646
647    #[test]
648    #[should_panic(expected = "CONTRACT BUILD ERROR")]
649    fn verify_bindings_tiered_panics_on_not_implemented() {
650        let (_dir, path) = write_temp_yaml(&yaml_with_not_implemented());
651        verify_bindings(&path, BindingPolicy::TieredEnforcement);
652    }
653
654    #[test]
655    #[should_panic(expected = "Cannot read binding YAML")]
656    fn verify_bindings_panics_on_missing_yaml() {
657        verify_bindings("/nonexistent/path/binding.yaml", BindingPolicy::WarnOnGaps);
658    }
659
660    #[test]
661    #[should_panic(expected = "Cannot parse binding YAML")]
662    fn verify_bindings_panics_on_invalid_yaml() {
663        let (_dir, path) = write_temp_yaml("not: [valid: {{yaml");
664        verify_bindings(&path, BindingPolicy::AllImplemented);
665    }
666
667    // ── verify_bindings: empty bindings list ──
668
669    #[test]
670    fn verify_bindings_empty_bindings() {
671        let yaml = r#"
672version: "1.0.0"
673target_crate: test_crate
674bindings: []
675"#;
676        let (_dir, path) = write_temp_yaml(yaml);
677        let result = verify_bindings(&path, BindingPolicy::AllImplemented);
678        assert_eq!(result.bound_count, 0);
679        assert_eq!(result.partial_count, 0);
680        assert_eq!(result.not_implemented_count, 0);
681    }
682
683    // ── verify_bindings with real binding file ──
684
685    #[test]
686    fn verify_bindings_warn_on_gaps_real_file() {
687        let binding_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
688            .join("../../contracts/aprender/binding.yaml");
689        let result = verify_bindings(binding_path.to_str().unwrap(), BindingPolicy::WarnOnGaps);
690        assert!(
691            result.bound_count > 0,
692            "Should have some implemented bindings"
693        );
694    }
695
696    // ── verify_bindings: rerun-if-changed with parent directories ──
697
698    #[test]
699    fn verify_bindings_handles_nested_path() {
700        // Tests the parent/grandparent rerun-if-changed logic
701        let dir = tempfile::tempdir().expect("create tempdir");
702        let nested = dir.path().join("contracts").join("crate");
703        std::fs::create_dir_all(&nested).expect("create nested dirs");
704        let yaml_path = nested.join("binding.yaml");
705        std::fs::write(&yaml_path, &yaml_all_implemented()).expect("write yaml");
706        let result = verify_bindings(yaml_path.to_str().unwrap(), BindingPolicy::AllImplemented);
707        assert_eq!(result.bound_count, 2);
708    }
709
710    #[test]
711    fn verify_bindings_partial_no_module_path() {
712        let yaml = r#"
713version: "1.0.0"
714target_crate: test_crate
715bindings:
716  - contract: relu-v1.yaml
717    equation: relu
718    status: partial
719"#;
720        let (_dir, path) = write_temp_yaml(yaml);
721        // WarnOnGaps should show "(?" for missing module_path
722        let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
723        assert_eq!(result.partial_count, 1);
724    }
725
726    #[test]
727    fn verify_bindings_not_implemented_no_module_path() {
728        let yaml = r#"
729version: "1.0.0"
730target_crate: test_crate
731bindings:
732  - contract: relu-v1.yaml
733    equation: relu
734    status: not_implemented
735"#;
736        let (_dir, path) = write_temp_yaml(yaml);
737        let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
738        assert_eq!(result.not_implemented_count, 1);
739    }
740
741    #[test]
742    fn verify_bindings_pending_no_module_path() {
743        let yaml = r#"
744version: "1.0.0"
745target_crate: test_crate
746bindings:
747  - contract: relu-v1.yaml
748    equation: relu
749    status: pending
750"#;
751        let (_dir, path) = write_temp_yaml(yaml);
752        let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
753        assert_eq!(result.not_implemented_count, 1);
754    }
755
756    // ── verify_source_functions tests ──
757
758    #[test]
759    fn verify_source_functions_missing_yaml() {
760        let missing = verify_source_functions("/nonexistent/binding.yaml", "src/", false);
761        assert!(missing.is_empty());
762    }
763
764    #[test]
765    fn verify_source_functions_invalid_yaml() {
766        let (_dir, path) = write_temp_yaml("not: [valid: {{yaml");
767        let missing = verify_source_functions(&path, "src/", false);
768        assert!(missing.is_empty());
769    }
770
771    #[test]
772    fn verify_source_functions_no_implemented_bindings() {
773        let yaml = r#"
774version: "1.0.0"
775target_crate: test_crate
776bindings:
777  - contract: gelu-v1.yaml
778    equation: gelu
779    status: not_implemented
780"#;
781        let (_dir, path) = write_temp_yaml(yaml);
782        let missing = verify_source_functions(&path, "src/", false);
783        assert!(missing.is_empty());
784    }
785
786    #[test]
787    fn verify_source_functions_all_found() {
788        let dir = tempfile::tempdir().expect("create tempdir");
789
790        // Write binding YAML with a function we'll create in source
791        let yaml = r#"
792version: "1.0.0"
793target_crate: test_crate
794bindings:
795  - contract: softmax-v1.yaml
796    equation: softmax
797    function: "test_crate::my_softmax"
798    status: implemented
799  - contract: relu-v1.yaml
800    equation: relu
801    function: "test_crate::my_relu"
802    status: implemented
803"#;
804        let yaml_path = dir.path().join("binding.yaml");
805        std::fs::write(&yaml_path, yaml).expect("write yaml");
806
807        // Write source files with matching pub fn declarations
808        let src_dir = dir.path().join("src");
809        std::fs::create_dir_all(&src_dir).expect("create src dir");
810        std::fs::write(
811            src_dir.join("lib.rs"),
812            "pub fn my_softmax(x: &[f32]) -> Vec<f32> { vec![] }\npub fn my_relu(x: f32) -> f32 { x }\n",
813        )
814        .expect("write lib.rs");
815
816        let missing = verify_source_functions(
817            yaml_path.to_str().unwrap(),
818            src_dir.to_str().unwrap(),
819            false,
820        );
821        assert!(
822            missing.is_empty(),
823            "All functions should be found: {missing:?}"
824        );
825    }
826
827    #[test]
828    fn verify_source_functions_some_missing() {
829        let dir = tempfile::tempdir().expect("create tempdir");
830
831        let yaml = r#"
832version: "1.0.0"
833target_crate: test_crate
834bindings:
835  - contract: softmax-v1.yaml
836    equation: softmax
837    function: "test_crate::my_softmax"
838    status: implemented
839  - contract: silu-v1.yaml
840    equation: silu
841    function: "test_crate::my_silu"
842    status: implemented
843"#;
844        let yaml_path = dir.path().join("binding.yaml");
845        std::fs::write(&yaml_path, yaml).expect("write yaml");
846
847        // Only provide my_softmax, not my_silu
848        let src_dir = dir.path().join("src");
849        std::fs::create_dir_all(&src_dir).expect("create src dir");
850        std::fs::write(
851            src_dir.join("lib.rs"),
852            "pub fn my_softmax(x: &[f32]) -> Vec<f32> { vec![] }\n",
853        )
854        .expect("write lib.rs");
855
856        let missing = verify_source_functions(
857            yaml_path.to_str().unwrap(),
858            src_dir.to_str().unwrap(),
859            false,
860        );
861        assert_eq!(missing.len(), 1);
862        assert!(missing.contains(&"my_silu".to_string()));
863    }
864
865    #[test]
866    #[should_panic(expected = "verify_source_functions")]
867    fn verify_source_functions_hard_fail_panics() {
868        let dir = tempfile::tempdir().expect("create tempdir");
869
870        let yaml = r#"
871version: "1.0.0"
872target_crate: test_crate
873bindings:
874  - contract: missing-v1.yaml
875    equation: missing
876    function: "test_crate::nonexistent_function"
877    status: implemented
878"#;
879        let yaml_path = dir.path().join("binding.yaml");
880        std::fs::write(&yaml_path, yaml).expect("write yaml");
881
882        let src_dir = dir.path().join("src");
883        std::fs::create_dir_all(&src_dir).expect("create src dir");
884        std::fs::write(src_dir.join("lib.rs"), "pub fn other() {}\n").expect("write lib.rs");
885
886        verify_source_functions(
887            yaml_path.to_str().unwrap(),
888            src_dir.to_str().unwrap(),
889            true, // hard_fail = true
890        );
891    }
892
893    #[test]
894    fn verify_source_functions_nonexistent_src_dir() {
895        let dir = tempfile::tempdir().expect("create tempdir");
896
897        let yaml = r#"
898version: "1.0.0"
899target_crate: test_crate
900bindings:
901  - contract: softmax-v1.yaml
902    equation: softmax
903    function: "test_crate::softmax"
904    status: implemented
905"#;
906        let yaml_path = dir.path().join("binding.yaml");
907        std::fs::write(&yaml_path, yaml).expect("write yaml");
908
909        // Point to a src_dir that doesn't exist
910        let missing = verify_source_functions(
911            yaml_path.to_str().unwrap(),
912            "/tmp/nonexistent_src_dir_test_provable",
913            false,
914        );
915        // Function should be missing since src_dir doesn't exist
916        assert!(!missing.is_empty());
917    }
918
919    #[test]
920    fn verify_source_functions_skips_not_implemented() {
921        let dir = tempfile::tempdir().expect("create tempdir");
922
923        let yaml = r#"
924version: "1.0.0"
925target_crate: test_crate
926bindings:
927  - contract: gelu-v1.yaml
928    equation: gelu
929    function: "test_crate::gelu"
930    status: not_implemented
931  - contract: silu-v1.yaml
932    equation: silu
933    function: "test_crate::silu"
934    status: partial
935"#;
936        let yaml_path = dir.path().join("binding.yaml");
937        std::fs::write(&yaml_path, yaml).expect("write yaml");
938
939        let src_dir = dir.path().join("src");
940        std::fs::create_dir_all(&src_dir).expect("create src dir");
941        std::fs::write(src_dir.join("lib.rs"), "").expect("write empty lib.rs");
942
943        // Only implemented bindings are checked
944        let missing = verify_source_functions(
945            yaml_path.to_str().unwrap(),
946            src_dir.to_str().unwrap(),
947            false,
948        );
949        assert!(
950            missing.is_empty(),
951            "Non-implemented bindings should be skipped"
952        );
953    }
954
955    #[test]
956    fn verify_source_functions_no_function_field() {
957        let dir = tempfile::tempdir().expect("create tempdir");
958
959        let yaml = r#"
960version: "1.0.0"
961target_crate: test_crate
962bindings:
963  - contract: softmax-v1.yaml
964    equation: softmax
965    status: implemented
966"#;
967        let yaml_path = dir.path().join("binding.yaml");
968        std::fs::write(&yaml_path, yaml).expect("write yaml");
969
970        // No function field means nothing to check
971        let missing = verify_source_functions(yaml_path.to_str().unwrap(), "/tmp/whatever", false);
972        assert!(missing.is_empty());
973    }
974
975    #[test]
976    fn verify_source_functions_many_missing_truncates() {
977        let dir = tempfile::tempdir().expect("create tempdir");
978
979        // Create > 10 implemented bindings so the output truncation triggers
980        let mut bindings = String::new();
981        for i in 0..15 {
982            bindings.push_str(&format!(
983                r#"  - contract: fn{i}-v1.yaml
984    equation: fn{i}
985    function: "test_crate::missing_fn_{i}"
986    status: implemented
987"#
988            ));
989        }
990        let yaml = format!(
991            r#"
992version: "1.0.0"
993target_crate: test_crate
994bindings:
995{bindings}"#
996        );
997        let yaml_path = dir.path().join("binding.yaml");
998        std::fs::write(&yaml_path, &yaml).expect("write yaml");
999
1000        let src_dir = dir.path().join("src");
1001        std::fs::create_dir_all(&src_dir).expect("create src dir");
1002        std::fs::write(src_dir.join("lib.rs"), "").expect("write empty lib.rs");
1003
1004        let missing = verify_source_functions(
1005            yaml_path.to_str().unwrap(),
1006            src_dir.to_str().unwrap(),
1007            false,
1008        );
1009        assert_eq!(missing.len(), 15);
1010    }
1011
1012    // ── scan_source_fns tests ──
1013
1014    #[test]
1015    fn scan_source_fns_empty_dir() {
1016        let dir = tempfile::tempdir().expect("create tempdir");
1017        let mut found = std::collections::HashSet::new();
1018        scan_source_fns(dir.path(), &mut found);
1019        assert!(found.is_empty());
1020    }
1021
1022    #[test]
1023    fn scan_source_fns_finds_pub_fn() {
1024        let dir = tempfile::tempdir().expect("create tempdir");
1025        std::fs::write(
1026            dir.path().join("lib.rs"),
1027            "pub fn my_function(x: i32) -> i32 { x }\n",
1028        )
1029        .expect("write");
1030        let mut found = std::collections::HashSet::new();
1031        scan_source_fns(dir.path(), &mut found);
1032        assert!(found.contains("my_function"));
1033    }
1034
1035    #[test]
1036    fn scan_source_fns_finds_pub_async_fn() {
1037        let dir = tempfile::tempdir().expect("create tempdir");
1038        std::fs::write(
1039            dir.path().join("lib.rs"),
1040            "pub async fn fetch_data() -> Vec<u8> { vec![] }\n",
1041        )
1042        .expect("write");
1043        let mut found = std::collections::HashSet::new();
1044        scan_source_fns(dir.path(), &mut found);
1045        assert!(found.contains("fetch_data"));
1046    }
1047
1048    #[test]
1049    fn scan_source_fns_finds_pub_crate_fn() {
1050        let dir = tempfile::tempdir().expect("create tempdir");
1051        std::fs::write(
1052            dir.path().join("lib.rs"),
1053            "pub(crate) fn internal_helper() {}\n",
1054        )
1055        .expect("write");
1056        let mut found = std::collections::HashSet::new();
1057        scan_source_fns(dir.path(), &mut found);
1058        assert!(found.contains("internal_helper"));
1059    }
1060
1061    #[test]
1062    fn scan_source_fns_ignores_private_fn() {
1063        let dir = tempfile::tempdir().expect("create tempdir");
1064        std::fs::write(dir.path().join("lib.rs"), "fn private_function() {}\n").expect("write");
1065        let mut found = std::collections::HashSet::new();
1066        scan_source_fns(dir.path(), &mut found);
1067        assert!(found.is_empty());
1068    }
1069
1070    #[test]
1071    fn scan_source_fns_skips_non_rs_files() {
1072        let dir = tempfile::tempdir().expect("create tempdir");
1073        std::fs::write(
1074            dir.path().join("data.txt"),
1075            "pub fn should_be_ignored() {}\n",
1076        )
1077        .expect("write");
1078        let mut found = std::collections::HashSet::new();
1079        scan_source_fns(dir.path(), &mut found);
1080        assert!(found.is_empty());
1081    }
1082
1083    #[test]
1084    fn scan_source_fns_recursive() {
1085        let dir = tempfile::tempdir().expect("create tempdir");
1086        let subdir = dir.path().join("submod");
1087        std::fs::create_dir_all(&subdir).expect("create subdir");
1088        std::fs::write(subdir.join("inner.rs"), "pub fn nested_function() {}\n").expect("write");
1089        let mut found = std::collections::HashSet::new();
1090        scan_source_fns(dir.path(), &mut found);
1091        assert!(found.contains("nested_function"));
1092    }
1093
1094    #[test]
1095    fn scan_source_fns_skips_target_dir() {
1096        let dir = tempfile::tempdir().expect("create tempdir");
1097        let target_dir = dir.path().join("target");
1098        std::fs::create_dir_all(&target_dir).expect("create target dir");
1099        std::fs::write(
1100            target_dir.join("generated.rs"),
1101            "pub fn should_be_skipped() {}\n",
1102        )
1103        .expect("write");
1104        let mut found = std::collections::HashSet::new();
1105        scan_source_fns(dir.path(), &mut found);
1106        assert!(found.is_empty());
1107    }
1108
1109    #[test]
1110    fn scan_source_fns_skips_git_dir() {
1111        let dir = tempfile::tempdir().expect("create tempdir");
1112        let git_dir = dir.path().join(".git");
1113        std::fs::create_dir_all(&git_dir).expect("create .git dir");
1114        std::fs::write(git_dir.join("hook.rs"), "pub fn should_be_skipped() {}\n").expect("write");
1115        let mut found = std::collections::HashSet::new();
1116        scan_source_fns(dir.path(), &mut found);
1117        assert!(found.is_empty());
1118    }
1119
1120    #[test]
1121    fn scan_source_fns_handles_generic_fn() {
1122        let dir = tempfile::tempdir().expect("create tempdir");
1123        std::fs::write(
1124            dir.path().join("lib.rs"),
1125            "pub fn generic_fn<T: Clone>(x: T) -> T { x }\n",
1126        )
1127        .expect("write");
1128        let mut found = std::collections::HashSet::new();
1129        scan_source_fns(dir.path(), &mut found);
1130        assert!(found.contains("generic_fn"));
1131    }
1132
1133    #[test]
1134    fn scan_source_fns_case_insensitive() {
1135        let dir = tempfile::tempdir().expect("create tempdir");
1136        std::fs::write(dir.path().join("lib.rs"), "pub fn MyMixedCase() {}\n").expect("write");
1137        let mut found = std::collections::HashSet::new();
1138        scan_source_fns(dir.path(), &mut found);
1139        assert!(found.contains("mymixedcase"), "should be lowercased");
1140    }
1141
1142    #[test]
1143    fn scan_source_fns_multiple_fns_one_file() {
1144        let dir = tempfile::tempdir().expect("create tempdir");
1145        std::fs::write(
1146            dir.path().join("lib.rs"),
1147            "pub fn alpha() {}\npub fn beta() {}\npub async fn gamma() {}\n",
1148        )
1149        .expect("write");
1150        let mut found = std::collections::HashSet::new();
1151        scan_source_fns(dir.path(), &mut found);
1152        assert!(found.contains("alpha"));
1153        assert!(found.contains("beta"));
1154        assert!(found.contains("gamma"));
1155        assert_eq!(found.len(), 3);
1156    }
1157
1158    #[test]
1159    fn scan_source_fns_nonexistent_dir() {
1160        let mut found = std::collections::HashSet::new();
1161        scan_source_fns(
1162            Path::new("/tmp/nonexistent_dir_test_provable_xyz"),
1163            &mut found,
1164        );
1165        assert!(found.is_empty());
1166    }
1167
1168    // ── verify_bindings: partial with module_path set ──
1169
1170    #[test]
1171    #[should_panic(expected = "status 'partial'")]
1172    fn verify_bindings_all_implemented_partial_with_module_path() {
1173        let yaml = r#"
1174version: "1.0.0"
1175target_crate: test_crate
1176bindings:
1177  - contract: relu-v1.yaml
1178    equation: relu
1179    module_path: "test::relu"
1180    function: my_relu
1181    status: partial
1182"#;
1183        let (_dir, path) = write_temp_yaml(yaml);
1184        verify_bindings(&path, BindingPolicy::AllImplemented);
1185    }
1186
1187    #[test]
1188    #[should_panic(expected = "status 'not_implemented'")]
1189    fn verify_bindings_all_implemented_not_impl_with_module_path() {
1190        let yaml = r#"
1191version: "1.0.0"
1192target_crate: test_crate
1193bindings:
1194  - contract: relu-v1.yaml
1195    equation: relu
1196    module_path: "test::relu"
1197    function: my_relu
1198    status: not_implemented
1199"#;
1200        let (_dir, path) = write_temp_yaml(yaml);
1201        verify_bindings(&path, BindingPolicy::AllImplemented);
1202    }
1203
1204    #[test]
1205    #[should_panic(expected = "status 'not_implemented'")]
1206    fn verify_bindings_tiered_not_impl_with_module_path() {
1207        let yaml = r#"
1208version: "1.0.0"
1209target_crate: test_crate
1210bindings:
1211  - contract: relu-v1.yaml
1212    equation: relu
1213    module_path: "test::relu"
1214    function: my_relu
1215    status: not_implemented
1216"#;
1217        let (_dir, path) = write_temp_yaml(yaml);
1218        verify_bindings(&path, BindingPolicy::TieredEnforcement);
1219    }
1220
1221    // ── verify_bindings: path with no parent (edge case) ──
1222
1223    #[test]
1224    #[should_panic(expected = "Cannot read binding YAML")]
1225    fn verify_bindings_bare_filename() {
1226        // A bare filename with no directory separators
1227        verify_bindings("nonexistent.yaml", BindingPolicy::WarnOnGaps);
1228    }
1229}