1use crate::inference::{OwnershipInference, OwnershipKind};
34use crate::ml_features::{InferredOwnership, OwnershipFeatures, OwnershipPrediction};
35use serde::{Deserialize, Serialize};
36
37pub const DEFAULT_CONFIDENCE_THRESHOLD: f64 = 0.65;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44pub enum ClassificationMethod {
45 RuleBased,
47 MachineLearning,
49 Fallback,
51 Hybrid,
53}
54
55impl std::fmt::Display for ClassificationMethod {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 match self {
58 ClassificationMethod::RuleBased => write!(f, "rule-based"),
59 ClassificationMethod::MachineLearning => write!(f, "ml"),
60 ClassificationMethod::Fallback => write!(f, "fallback"),
61 ClassificationMethod::Hybrid => write!(f, "hybrid"),
62 }
63 }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct HybridResult {
69 pub variable: String,
71 pub ownership: InferredOwnership,
73 pub confidence: f64,
75 pub method: ClassificationMethod,
77 pub rule_result: Option<InferredOwnership>,
79 pub ml_result: Option<OwnershipPrediction>,
81 pub reasoning: String,
83}
84
85impl HybridResult {
86 pub fn used_fallback(&self) -> bool {
88 self.method == ClassificationMethod::Fallback
89 }
90
91 pub fn ml_rejected(&self) -> bool {
93 self.ml_result.is_some() && self.method == ClassificationMethod::Fallback
94 }
95}
96
97pub trait OwnershipModel: Send + Sync {
102 fn predict(&self, features: &OwnershipFeatures) -> OwnershipPrediction;
104
105 fn predict_batch(&self, features: &[OwnershipFeatures]) -> Vec<OwnershipPrediction> {
107 features.iter().map(|f| self.predict(f)).collect()
108 }
109
110 fn name(&self) -> &str {
112 "unknown"
113 }
114}
115
116#[derive(Debug, Clone, Default)]
120pub struct NullModel;
121
122impl OwnershipModel for NullModel {
123 fn predict(&self, _features: &OwnershipFeatures) -> OwnershipPrediction {
124 OwnershipPrediction {
125 kind: InferredOwnership::RawPointer,
126 confidence: 0.0,
127 fallback: None,
128 }
129 }
130
131 fn name(&self) -> &str {
132 "null"
133 }
134}
135
136#[derive(Debug)]
138pub struct HybridClassifier {
139 confidence_threshold: f64,
141 ml_enabled: bool,
143}
144
145impl Default for HybridClassifier {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151impl HybridClassifier {
152 pub fn new() -> Self {
154 Self {
155 confidence_threshold: DEFAULT_CONFIDENCE_THRESHOLD,
156 ml_enabled: false,
157 }
158 }
159
160 pub fn with_threshold(threshold: f64) -> Self {
162 Self {
163 confidence_threshold: threshold.clamp(0.0, 1.0),
164 ml_enabled: false,
165 }
166 }
167
168 pub fn enable_ml(&mut self) {
170 self.ml_enabled = true;
171 }
172
173 pub fn disable_ml(&mut self) {
175 self.ml_enabled = false;
176 }
177
178 pub fn ml_enabled(&self) -> bool {
180 self.ml_enabled
181 }
182
183 pub fn confidence_threshold(&self) -> f64 {
185 self.confidence_threshold
186 }
187
188 pub fn set_threshold(&mut self, threshold: f64) {
190 self.confidence_threshold = threshold.clamp(0.0, 1.0);
191 }
192
193 pub fn classify_rule_based(&self, inference: &OwnershipInference) -> HybridResult {
195 let ownership = ownership_kind_to_inferred(&inference.kind);
196
197 HybridResult {
198 variable: inference.variable.clone(),
199 ownership,
200 confidence: inference.confidence as f64,
201 method: ClassificationMethod::RuleBased,
202 rule_result: Some(ownership),
203 ml_result: None,
204 reasoning: format!("Rule-based: {}", inference.reason),
205 }
206 }
207
208 pub fn classify_hybrid<M: OwnershipModel>(
210 &self,
211 inference: &OwnershipInference,
212 features: &OwnershipFeatures,
213 model: &M,
214 ) -> HybridResult {
215 let rule_ownership = ownership_kind_to_inferred(&inference.kind);
217
218 if !self.ml_enabled {
220 return self.classify_rule_based(inference);
221 }
222
223 let ml_prediction = model.predict(features);
225 let ml_conf = ml_prediction.confidence as f64;
226 let ml_kind = ml_prediction.kind;
227
228 if ml_conf >= self.confidence_threshold {
230 HybridResult {
232 variable: inference.variable.clone(),
233 ownership: ml_kind,
234 confidence: ml_conf,
235 method: ClassificationMethod::MachineLearning,
236 rule_result: Some(rule_ownership),
237 ml_result: Some(ml_prediction),
238 reasoning: format!("ML prediction (confidence {:.2}): {:?}", ml_conf, ml_kind),
239 }
240 } else {
241 HybridResult {
243 variable: inference.variable.clone(),
244 ownership: rule_ownership,
245 confidence: inference.confidence as f64,
246 method: ClassificationMethod::Fallback,
247 rule_result: Some(rule_ownership),
248 ml_result: Some(ml_prediction),
249 reasoning: format!(
250 "Fallback to rules (ML confidence {:.2} < threshold {:.2}): {}",
251 ml_conf, self.confidence_threshold, inference.reason
252 ),
253 }
254 }
255 }
256
257 pub fn classify_ensemble<M: OwnershipModel>(
262 &self,
263 inference: &OwnershipInference,
264 features: &OwnershipFeatures,
265 model: &M,
266 ) -> HybridResult {
267 let rule_ownership = ownership_kind_to_inferred(&inference.kind);
268 let ml_prediction = model.predict(features);
269 let ml_conf = ml_prediction.confidence as f64;
270 let ml_kind = ml_prediction.kind;
271
272 let agree = rule_ownership == ml_kind;
274
275 if agree {
276 let combined_confidence = (inference.confidence as f64 + ml_conf) / 2.0 * 1.1;
278 let final_confidence = combined_confidence.min(1.0);
279
280 HybridResult {
281 variable: inference.variable.clone(),
282 ownership: rule_ownership,
283 confidence: final_confidence,
284 method: ClassificationMethod::Hybrid,
285 rule_result: Some(rule_ownership),
286 ml_result: Some(ml_prediction),
287 reasoning: format!(
288 "Hybrid (rules + ML agree): boosted confidence {:.2}",
289 final_confidence
290 ),
291 }
292 } else {
293 if ml_conf > inference.confidence as f64 {
295 HybridResult {
296 variable: inference.variable.clone(),
297 ownership: ml_kind,
298 confidence: ml_conf,
299 method: ClassificationMethod::MachineLearning,
300 rule_result: Some(rule_ownership),
301 ml_result: Some(ml_prediction),
302 reasoning: format!(
303 "ML wins (conf {:.2} > rules {:.2}): {:?}",
304 ml_conf, inference.confidence, ml_kind
305 ),
306 }
307 } else {
308 HybridResult {
309 variable: inference.variable.clone(),
310 ownership: rule_ownership,
311 confidence: inference.confidence as f64,
312 method: ClassificationMethod::RuleBased,
313 rule_result: Some(rule_ownership),
314 ml_result: Some(ml_prediction),
315 reasoning: format!(
316 "Rules win (conf {:.2} > ML {:.2}): {}",
317 inference.confidence, ml_conf, inference.reason
318 ),
319 }
320 }
321 }
322 }
323}
324
325fn ownership_kind_to_inferred(kind: &OwnershipKind) -> InferredOwnership {
327 match kind {
328 OwnershipKind::Owning => InferredOwnership::Owned,
329 OwnershipKind::ImmutableBorrow => InferredOwnership::Borrowed,
330 OwnershipKind::MutableBorrow => InferredOwnership::BorrowedMut,
331 OwnershipKind::ArrayPointer { .. } => InferredOwnership::Slice,
332 OwnershipKind::Unknown => InferredOwnership::RawPointer,
333 }
334}
335
336#[derive(Debug, Clone, Default, Serialize, Deserialize)]
338pub struct HybridMetrics {
339 pub total: u64,
341 pub rule_based: u64,
343 pub ml_used: u64,
345 pub fallback: u64,
347 pub hybrid: u64,
349 pub agreements: u64,
351 pub disagreements: u64,
353}
354
355impl HybridMetrics {
356 pub fn new() -> Self {
358 Self::default()
359 }
360
361 pub fn record(&mut self, result: &HybridResult) {
363 self.total += 1;
364 match result.method {
365 ClassificationMethod::RuleBased => self.rule_based += 1,
366 ClassificationMethod::MachineLearning => self.ml_used += 1,
367 ClassificationMethod::Fallback => self.fallback += 1,
368 ClassificationMethod::Hybrid => self.hybrid += 1,
369 }
370
371 if let (Some(rule), Some(ml)) = (&result.rule_result, &result.ml_result) {
373 if *rule == ml.kind {
374 self.agreements += 1;
375 } else {
376 self.disagreements += 1;
377 }
378 }
379 }
380
381 pub fn ml_usage_rate(&self) -> f64 {
383 if self.total == 0 {
384 0.0
385 } else {
386 self.ml_used as f64 / self.total as f64
387 }
388 }
389
390 pub fn fallback_rate(&self) -> f64 {
392 if self.total == 0 {
393 0.0
394 } else {
395 self.fallback as f64 / self.total as f64
396 }
397 }
398
399 pub fn agreement_rate(&self) -> f64 {
401 let both = self.agreements + self.disagreements;
402 if both == 0 {
403 1.0 } else {
405 self.agreements as f64 / both as f64
406 }
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 #[test]
419 fn classification_method_display() {
420 assert_eq!(ClassificationMethod::RuleBased.to_string(), "rule-based");
421 assert_eq!(ClassificationMethod::MachineLearning.to_string(), "ml");
422 assert_eq!(ClassificationMethod::Fallback.to_string(), "fallback");
423 assert_eq!(ClassificationMethod::Hybrid.to_string(), "hybrid");
424 }
425
426 #[test]
431 fn hybrid_classifier_default() {
432 let classifier = HybridClassifier::new();
433 assert!(!classifier.ml_enabled());
434 assert!((classifier.confidence_threshold() - 0.65).abs() < 0.001);
435 }
436
437 #[test]
438 fn hybrid_classifier_with_threshold() {
439 let classifier = HybridClassifier::with_threshold(0.8);
440 assert!((classifier.confidence_threshold() - 0.8).abs() < 0.001);
441 }
442
443 #[test]
444 fn hybrid_classifier_threshold_clamp() {
445 let low = HybridClassifier::with_threshold(-0.5);
446 assert!((low.confidence_threshold() - 0.0).abs() < 0.001);
447
448 let high = HybridClassifier::with_threshold(1.5);
449 assert!((high.confidence_threshold() - 1.0).abs() < 0.001);
450 }
451
452 #[test]
453 fn hybrid_classifier_enable_disable_ml() {
454 let mut classifier = HybridClassifier::new();
455 assert!(!classifier.ml_enabled());
456
457 classifier.enable_ml();
458 assert!(classifier.ml_enabled());
459
460 classifier.disable_ml();
461 assert!(!classifier.ml_enabled());
462 }
463
464 #[test]
465 fn classify_rule_based_owning() {
466 let classifier = HybridClassifier::new();
467 let inference = OwnershipInference {
468 variable: "ptr".to_string(),
469 kind: OwnershipKind::Owning,
470 confidence: 0.9,
471 reason: "malloc detected".to_string(),
472 };
473
474 let result = classifier.classify_rule_based(&inference);
475
476 assert_eq!(result.variable, "ptr");
477 assert_eq!(result.ownership, InferredOwnership::Owned);
478 assert_eq!(result.method, ClassificationMethod::RuleBased);
479 assert!(result.ml_result.is_none());
480 }
481
482 #[test]
483 fn classify_rule_based_immutable_borrow() {
484 let classifier = HybridClassifier::new();
485 let inference = OwnershipInference {
486 variable: "ref".to_string(),
487 kind: OwnershipKind::ImmutableBorrow,
488 confidence: 0.85,
489 reason: "read-only access".to_string(),
490 };
491
492 let result = classifier.classify_rule_based(&inference);
493
494 assert_eq!(result.ownership, InferredOwnership::Borrowed);
495 }
496
497 struct MockModel {
503 ownership: InferredOwnership,
504 confidence: f64,
505 }
506
507 impl MockModel {
508 fn with_confidence(ownership: InferredOwnership, confidence: f64) -> Self {
509 Self {
510 ownership,
511 confidence,
512 }
513 }
514 }
515
516 impl OwnershipModel for MockModel {
517 fn predict(&self, _features: &OwnershipFeatures) -> OwnershipPrediction {
518 OwnershipPrediction {
519 kind: self.ownership,
520 confidence: self.confidence as f32,
521 fallback: None,
522 }
523 }
524
525 fn name(&self) -> &str {
526 "mock"
527 }
528 }
529
530 #[test]
531 fn classify_hybrid_ml_high_confidence() {
532 let mut classifier = HybridClassifier::new();
533 classifier.enable_ml();
534
535 let inference = OwnershipInference {
536 variable: "ptr".to_string(),
537 kind: OwnershipKind::Unknown,
538 confidence: 0.5,
539 reason: "uncertain".to_string(),
540 };
541
542 let features = OwnershipFeatures::default();
543 let model = MockModel::with_confidence(InferredOwnership::Vec, 0.9);
544
545 let result = classifier.classify_hybrid(&inference, &features, &model);
546
547 assert_eq!(result.ownership, InferredOwnership::Vec);
548 assert_eq!(result.method, ClassificationMethod::MachineLearning);
549 assert!(!result.used_fallback());
550 }
551
552 #[test]
553 fn classify_hybrid_ml_low_confidence_fallback() {
554 let mut classifier = HybridClassifier::new();
555 classifier.enable_ml();
556
557 let inference = OwnershipInference {
558 variable: "ptr".to_string(),
559 kind: OwnershipKind::Owning,
560 confidence: 0.8,
561 reason: "malloc detected".to_string(),
562 };
563
564 let features = OwnershipFeatures::default();
565 let model = MockModel::with_confidence(InferredOwnership::Borrowed, 0.3);
566
567 let result = classifier.classify_hybrid(&inference, &features, &model);
568
569 assert_eq!(result.ownership, InferredOwnership::Owned);
571 assert_eq!(result.method, ClassificationMethod::Fallback);
572 assert!(result.used_fallback());
573 assert!(result.ml_rejected());
574 }
575
576 #[test]
577 fn classify_hybrid_ml_disabled() {
578 let classifier = HybridClassifier::new(); let inference = OwnershipInference {
581 variable: "ptr".to_string(),
582 kind: OwnershipKind::MutableBorrow,
583 confidence: 0.7,
584 reason: "mutation detected".to_string(),
585 };
586
587 let features = OwnershipFeatures::default();
588 let model = MockModel::with_confidence(InferredOwnership::Owned, 0.99);
589
590 let result = classifier.classify_hybrid(&inference, &features, &model);
591
592 assert_eq!(result.ownership, InferredOwnership::BorrowedMut);
594 assert_eq!(result.method, ClassificationMethod::RuleBased);
595 }
596
597 #[test]
602 fn classify_ensemble_agreement_boosts_confidence() {
603 let classifier = HybridClassifier::new();
604
605 let inference = OwnershipInference {
606 variable: "ptr".to_string(),
607 kind: OwnershipKind::Owning,
608 confidence: 0.7,
609 reason: "malloc".to_string(),
610 };
611
612 let features = OwnershipFeatures::default();
613 let model = MockModel::with_confidence(InferredOwnership::Owned, 0.8);
614
615 let result = classifier.classify_ensemble(&inference, &features, &model);
616
617 assert_eq!(result.method, ClassificationMethod::Hybrid);
618 assert!(result.confidence > 0.82);
620 }
621
622 #[test]
623 fn classify_ensemble_disagreement_ml_wins() {
624 let classifier = HybridClassifier::new();
625
626 let inference = OwnershipInference {
627 variable: "ptr".to_string(),
628 kind: OwnershipKind::Unknown,
629 confidence: 0.3,
630 reason: "unknown".to_string(),
631 };
632
633 let features = OwnershipFeatures::default();
634 let model = MockModel::with_confidence(InferredOwnership::Vec, 0.9);
635
636 let result = classifier.classify_ensemble(&inference, &features, &model);
637
638 assert_eq!(result.ownership, InferredOwnership::Vec);
639 assert_eq!(result.method, ClassificationMethod::MachineLearning);
640 }
641
642 #[test]
643 fn classify_ensemble_disagreement_rules_win() {
644 let classifier = HybridClassifier::new();
645
646 let inference = OwnershipInference {
647 variable: "ptr".to_string(),
648 kind: OwnershipKind::Owning,
649 confidence: 0.95,
650 reason: "malloc + free".to_string(),
651 };
652
653 let features = OwnershipFeatures::default();
654 let model = MockModel::with_confidence(InferredOwnership::Borrowed, 0.4);
655
656 let result = classifier.classify_ensemble(&inference, &features, &model);
657
658 assert_eq!(result.ownership, InferredOwnership::Owned);
659 assert_eq!(result.method, ClassificationMethod::RuleBased);
660 }
661
662 #[test]
667 fn null_model_returns_unknown() {
668 let model = NullModel;
669 let features = OwnershipFeatures::default();
670
671 let prediction = model.predict(&features);
672
673 assert_eq!(prediction.kind, InferredOwnership::RawPointer);
674 assert!((prediction.confidence as f64 - 0.0).abs() < 0.001);
675 }
676
677 #[test]
678 fn null_model_name() {
679 let model = NullModel;
680 assert_eq!(model.name(), "null");
681 }
682
683 #[test]
688 fn hybrid_result_used_fallback() {
689 let result = HybridResult {
690 variable: "x".to_string(),
691 ownership: InferredOwnership::Owned,
692 confidence: 0.8,
693 method: ClassificationMethod::Fallback,
694 rule_result: Some(InferredOwnership::Owned),
695 ml_result: Some(OwnershipPrediction {
696 kind: InferredOwnership::Borrowed,
697 confidence: 0.3,
698 fallback: None,
699 }),
700 reasoning: "test".to_string(),
701 };
702
703 assert!(result.used_fallback());
704 assert!(result.ml_rejected());
705 }
706
707 #[test]
708 fn hybrid_result_ml_not_rejected() {
709 let result = HybridResult {
710 variable: "x".to_string(),
711 ownership: InferredOwnership::Owned,
712 confidence: 0.9,
713 method: ClassificationMethod::MachineLearning,
714 rule_result: Some(InferredOwnership::RawPointer),
715 ml_result: Some(OwnershipPrediction {
716 kind: InferredOwnership::Owned,
717 confidence: 0.9,
718 fallback: None,
719 }),
720 reasoning: "test".to_string(),
721 };
722
723 assert!(!result.used_fallback());
724 assert!(!result.ml_rejected());
725 }
726
727 #[test]
732 fn hybrid_metrics_default() {
733 let metrics = HybridMetrics::new();
734 assert_eq!(metrics.total, 0);
735 assert_eq!(metrics.ml_usage_rate(), 0.0);
736 }
737
738 #[test]
739 fn hybrid_metrics_record() {
740 let mut metrics = HybridMetrics::new();
741
742 let result1 = HybridResult {
743 variable: "a".to_string(),
744 ownership: InferredOwnership::Owned,
745 confidence: 0.9,
746 method: ClassificationMethod::MachineLearning,
747 rule_result: Some(InferredOwnership::Owned),
748 ml_result: Some(OwnershipPrediction {
749 kind: InferredOwnership::Owned,
750 confidence: 0.9,
751 fallback: None,
752 }),
753 reasoning: "ml".to_string(),
754 };
755
756 let result2 = HybridResult {
757 variable: "b".to_string(),
758 ownership: InferredOwnership::Borrowed,
759 confidence: 0.7,
760 method: ClassificationMethod::RuleBased,
761 rule_result: Some(InferredOwnership::Borrowed),
762 ml_result: None,
763 reasoning: "rules".to_string(),
764 };
765
766 metrics.record(&result1);
767 metrics.record(&result2);
768
769 assert_eq!(metrics.total, 2);
770 assert_eq!(metrics.ml_used, 1);
771 assert_eq!(metrics.rule_based, 1);
772 assert!((metrics.ml_usage_rate() - 0.5).abs() < 0.001);
773 }
774
775 #[test]
776 fn hybrid_metrics_agreement_rate() {
777 let mut metrics = HybridMetrics::new();
778
779 let agree = HybridResult {
781 variable: "a".to_string(),
782 ownership: InferredOwnership::Owned,
783 confidence: 0.9,
784 method: ClassificationMethod::Hybrid,
785 rule_result: Some(InferredOwnership::Owned),
786 ml_result: Some(OwnershipPrediction {
787 kind: InferredOwnership::Owned,
788 confidence: 0.9,
789 fallback: None,
790 }),
791 reasoning: "agree".to_string(),
792 };
793
794 let disagree = HybridResult {
796 variable: "b".to_string(),
797 ownership: InferredOwnership::Owned,
798 confidence: 0.9,
799 method: ClassificationMethod::Fallback,
800 rule_result: Some(InferredOwnership::Owned),
801 ml_result: Some(OwnershipPrediction {
802 kind: InferredOwnership::Borrowed,
803 confidence: 0.3,
804 fallback: None,
805 }),
806 reasoning: "disagree".to_string(),
807 };
808
809 metrics.record(&agree);
810 metrics.record(&disagree);
811
812 assert_eq!(metrics.agreements, 1);
813 assert_eq!(metrics.disagreements, 1);
814 assert!((metrics.agreement_rate() - 0.5).abs() < 0.001);
815 }
816
817 #[test]
818 fn hybrid_metrics_fallback_rate() {
819 let mut metrics = HybridMetrics::new();
820 metrics.total = 10;
821 metrics.fallback = 3;
822
823 assert!((metrics.fallback_rate() - 0.3).abs() < 0.001);
824 }
825
826 #[test]
831 fn convert_ownership_kinds() {
832 assert_eq!(
833 ownership_kind_to_inferred(&OwnershipKind::Owning),
834 InferredOwnership::Owned
835 );
836 assert_eq!(
837 ownership_kind_to_inferred(&OwnershipKind::ImmutableBorrow),
838 InferredOwnership::Borrowed
839 );
840 assert_eq!(
841 ownership_kind_to_inferred(&OwnershipKind::MutableBorrow),
842 InferredOwnership::BorrowedMut
843 );
844 assert_eq!(
845 ownership_kind_to_inferred(&OwnershipKind::Unknown),
846 InferredOwnership::RawPointer
847 );
848 }
849
850 #[test]
851 fn convert_array_pointer() {
852 let array_kind = OwnershipKind::ArrayPointer {
853 base_array: "arr".to_string(),
854 element_type: decy_hir::HirType::Int,
855 base_index: Some(0),
856 };
857 assert_eq!(
858 ownership_kind_to_inferred(&array_kind),
859 InferredOwnership::Slice
860 );
861 }
862
863 #[test]
868 fn classify_ensemble_agreement_confidence_capped_at_one() {
869 let classifier = HybridClassifier::new();
871
872 let inference = OwnershipInference {
873 variable: "ptr".to_string(),
874 kind: OwnershipKind::Owning,
875 confidence: 0.99, reason: "malloc + free".to_string(),
877 };
878
879 let features = OwnershipFeatures::default();
880 let model = MockModel::with_confidence(InferredOwnership::Owned, 0.99);
882
883 let result = classifier.classify_ensemble(&inference, &features, &model);
884
885 assert_eq!(result.method, ClassificationMethod::Hybrid);
886 assert_eq!(result.ownership, InferredOwnership::Owned);
887 assert!((result.confidence - 1.0).abs() < 0.001);
889 assert!(result.reasoning.contains("boosted"));
890 }
891
892 #[test]
893 fn classify_ensemble_agreement_low_confidence_boosted() {
894 let classifier = HybridClassifier::new();
895
896 let inference = OwnershipInference {
897 variable: "ptr".to_string(),
898 kind: OwnershipKind::ImmutableBorrow,
899 confidence: 0.4,
900 reason: "parameter read-only".to_string(),
901 };
902
903 let features = OwnershipFeatures::default();
904 let model = MockModel::with_confidence(InferredOwnership::Borrowed, 0.5);
905
906 let result = classifier.classify_ensemble(&inference, &features, &model);
907
908 assert_eq!(result.method, ClassificationMethod::Hybrid);
909 assert_eq!(result.ownership, InferredOwnership::Borrowed);
910 assert!(result.confidence > 0.49);
912 assert!(result.confidence < 0.51);
913 }
914
915 #[test]
916 fn classify_ensemble_disagreement_ml_wins_with_exact_equality() {
917 let classifier = HybridClassifier::new();
919
920 let inference = OwnershipInference {
921 variable: "ptr".to_string(),
922 kind: OwnershipKind::Owning,
923 confidence: 0.7, reason: "allocation".to_string(),
925 };
926
927 let features = OwnershipFeatures::default();
928 let model = MockModel::with_confidence(InferredOwnership::Borrowed, 0.7);
929
930 let result = classifier.classify_ensemble(&inference, &features, &model);
931
932 assert_eq!(result.method, ClassificationMethod::RuleBased);
934 assert_eq!(result.ownership, InferredOwnership::Owned);
935 assert!(result.reasoning.contains("Rules win"));
936 }
937
938 #[test]
939 fn classify_ensemble_disagreement_ml_wins_clearly() {
940 let classifier = HybridClassifier::new();
941
942 let inference = OwnershipInference {
943 variable: "ptr".to_string(),
944 kind: OwnershipKind::Unknown, confidence: 0.3,
946 reason: "unknown pattern".to_string(),
947 };
948
949 let features = OwnershipFeatures::default();
950 let model = MockModel::with_confidence(InferredOwnership::Vec, 0.95);
951
952 let result = classifier.classify_ensemble(&inference, &features, &model);
953
954 assert_eq!(result.method, ClassificationMethod::MachineLearning);
955 assert_eq!(result.ownership, InferredOwnership::Vec);
956 assert!((result.confidence - 0.95).abs() < 0.001);
957 assert!(result.reasoning.contains("ML wins"));
958 }
959
960 #[test]
961 fn classify_ensemble_disagreement_rules_win_clearly() {
962 let classifier = HybridClassifier::new();
963
964 let inference = OwnershipInference {
965 variable: "ptr".to_string(),
966 kind: OwnershipKind::Owning,
967 confidence: 0.95,
968 reason: "malloc with free".to_string(),
969 };
970
971 let features = OwnershipFeatures::default();
972 let model = MockModel::with_confidence(InferredOwnership::Borrowed, 0.2);
973
974 let result = classifier.classify_ensemble(&inference, &features, &model);
975
976 assert_eq!(result.method, ClassificationMethod::RuleBased);
977 assert_eq!(result.ownership, InferredOwnership::Owned);
978 assert!((result.confidence - 0.95).abs() < 0.001);
979 assert!(result.reasoning.contains("Rules win"));
980 assert!(result.ml_result.is_some());
981 assert!(result.rule_result.is_some());
982 }
983
984 #[test]
985 fn classify_ensemble_all_ownership_kinds() {
986 let classifier = HybridClassifier::new();
988 let features = OwnershipFeatures::default();
989
990 let inference_mut = OwnershipInference {
992 variable: "ptr".to_string(),
993 kind: OwnershipKind::MutableBorrow,
994 confidence: 0.7,
995 reason: "mutation detected".to_string(),
996 };
997 let model_agree = MockModel::with_confidence(InferredOwnership::BorrowedMut, 0.8);
998 let result = classifier.classify_ensemble(&inference_mut, &features, &model_agree);
999 assert_eq!(result.method, ClassificationMethod::Hybrid);
1000 assert_eq!(result.ownership, InferredOwnership::BorrowedMut);
1001
1002 let inference_unknown = OwnershipInference {
1004 variable: "ptr".to_string(),
1005 kind: OwnershipKind::Unknown,
1006 confidence: 0.3,
1007 reason: "uncertain".to_string(),
1008 };
1009 let model_agree_raw = MockModel::with_confidence(InferredOwnership::RawPointer, 0.4);
1010 let result = classifier.classify_ensemble(&inference_unknown, &features, &model_agree_raw);
1011 assert_eq!(result.method, ClassificationMethod::Hybrid);
1012 assert_eq!(result.ownership, InferredOwnership::RawPointer);
1013
1014 let inference_immut = OwnershipInference {
1016 variable: "data".to_string(),
1017 kind: OwnershipKind::ImmutableBorrow,
1018 confidence: 0.85,
1019 reason: "read-only access".to_string(),
1020 };
1021 let model_agree_borrow = MockModel::with_confidence(InferredOwnership::Borrowed, 0.9);
1022 let result =
1023 classifier.classify_ensemble(&inference_immut, &features, &model_agree_borrow);
1024 assert_eq!(result.method, ClassificationMethod::Hybrid);
1025 assert_eq!(result.ownership, InferredOwnership::Borrowed);
1026
1027 let inference_arr = OwnershipInference {
1029 variable: "arr".to_string(),
1030 kind: OwnershipKind::ArrayPointer {
1031 base_array: "arr".to_string(),
1032 element_type: decy_hir::HirType::Int,
1033 base_index: Some(0),
1034 },
1035 confidence: 0.75,
1036 reason: "array parameter".to_string(),
1037 };
1038 let model_agree_slice = MockModel::with_confidence(InferredOwnership::Slice, 0.8);
1039 let result =
1040 classifier.classify_ensemble(&inference_arr, &features, &model_agree_slice);
1041 assert_eq!(result.method, ClassificationMethod::Hybrid);
1042 assert_eq!(result.ownership, InferredOwnership::Slice);
1043 }
1044
1045 #[test]
1046 fn classify_ensemble_immutable_borrow_disagreement() {
1047 let classifier = HybridClassifier::new();
1049 let features = OwnershipFeatures::default();
1050 let inference = OwnershipInference {
1051 variable: "ptr".to_string(),
1052 kind: OwnershipKind::ImmutableBorrow,
1053 confidence: 0.5,
1054 reason: "read-only".to_string(),
1055 };
1056 let model = MockModel::with_confidence(InferredOwnership::Owned, 0.9);
1057 let result = classifier.classify_ensemble(&inference, &features, &model);
1058 assert_eq!(result.method, ClassificationMethod::MachineLearning);
1059 assert_eq!(result.ownership, InferredOwnership::Owned);
1060 }
1061
1062 #[test]
1063 fn classify_ensemble_array_pointer_disagree_rules_win() {
1064 let classifier = HybridClassifier::new();
1066 let features = OwnershipFeatures::default();
1067 let inference = OwnershipInference {
1068 variable: "buf".to_string(),
1069 kind: OwnershipKind::ArrayPointer {
1070 base_array: "buf".to_string(),
1071 element_type: decy_hir::HirType::Char,
1072 base_index: None,
1073 },
1074 confidence: 0.9,
1075 reason: "array pattern".to_string(),
1076 };
1077 let model = MockModel::with_confidence(InferredOwnership::BorrowedMut, 0.3);
1078 let result = classifier.classify_ensemble(&inference, &features, &model);
1079 assert_eq!(result.method, ClassificationMethod::RuleBased);
1080 assert_eq!(result.ownership, InferredOwnership::Slice);
1081 }
1082
1083 #[test]
1088 fn classify_hybrid_at_exact_threshold() {
1089 let mut classifier = HybridClassifier::with_threshold(0.5);
1093 classifier.enable_ml();
1094
1095 let inference = OwnershipInference {
1096 variable: "ptr".to_string(),
1097 kind: OwnershipKind::Unknown,
1098 confidence: 0.3,
1099 reason: "uncertain".to_string(),
1100 };
1101
1102 let features = OwnershipFeatures::default();
1103 let model = MockModel::with_confidence(InferredOwnership::Owned, 0.75);
1104
1105 let result = classifier.classify_hybrid(&inference, &features, &model);
1106
1107 assert_eq!(result.method, ClassificationMethod::MachineLearning);
1109 assert_eq!(result.ownership, InferredOwnership::Owned);
1110 assert!(result.reasoning.contains("ML prediction"));
1111 }
1112
1113 #[test]
1114 fn classify_hybrid_just_below_threshold() {
1115 let mut classifier = HybridClassifier::with_threshold(0.65);
1116 classifier.enable_ml();
1117
1118 let inference = OwnershipInference {
1119 variable: "ptr".to_string(),
1120 kind: OwnershipKind::Owning,
1121 confidence: 0.9,
1122 reason: "malloc".to_string(),
1123 };
1124
1125 let features = OwnershipFeatures::default();
1126 let model = MockModel::with_confidence(InferredOwnership::Borrowed, 0.6499);
1127
1128 let result = classifier.classify_hybrid(&inference, &features, &model);
1129
1130 assert_eq!(result.method, ClassificationMethod::Fallback);
1132 assert_eq!(result.ownership, InferredOwnership::Owned);
1133 assert!(result.reasoning.contains("Fallback"));
1134 }
1135
1136 #[test]
1141 fn set_threshold_clamps() {
1142 let mut classifier = HybridClassifier::new();
1143 classifier.set_threshold(2.0);
1144 assert!((classifier.confidence_threshold() - 1.0).abs() < 0.001);
1145
1146 classifier.set_threshold(-1.0);
1147 assert!((classifier.confidence_threshold() - 0.0).abs() < 0.001);
1148
1149 classifier.set_threshold(0.42);
1150 assert!((classifier.confidence_threshold() - 0.42).abs() < 0.001);
1151 }
1152
1153 #[test]
1158 fn hybrid_metrics_record_hybrid_method() {
1159 let mut metrics = HybridMetrics::new();
1160 let result = HybridResult {
1161 variable: "x".to_string(),
1162 ownership: InferredOwnership::Owned,
1163 confidence: 0.9,
1164 method: ClassificationMethod::Hybrid,
1165 rule_result: Some(InferredOwnership::Owned),
1166 ml_result: Some(OwnershipPrediction {
1167 kind: InferredOwnership::Owned,
1168 confidence: 0.9,
1169 fallback: None,
1170 }),
1171 reasoning: "hybrid agreement".to_string(),
1172 };
1173 metrics.record(&result);
1174
1175 assert_eq!(metrics.hybrid, 1);
1176 assert_eq!(metrics.total, 1);
1177 assert_eq!(metrics.agreements, 1);
1178 assert_eq!(metrics.disagreements, 0);
1179 }
1180
1181 #[test]
1182 fn hybrid_metrics_agreement_rate_no_comparisons() {
1183 let metrics = HybridMetrics::new();
1184 assert!((metrics.agreement_rate() - 1.0).abs() < 0.001);
1186 }
1187
1188 #[test]
1189 fn hybrid_metrics_all_methods_tracked() {
1190 let mut metrics = HybridMetrics::new();
1191
1192 for (method, rule_own, ml_own) in [
1194 (ClassificationMethod::RuleBased, InferredOwnership::Owned, InferredOwnership::Owned),
1195 (ClassificationMethod::MachineLearning, InferredOwnership::Borrowed, InferredOwnership::Borrowed),
1196 (ClassificationMethod::Fallback, InferredOwnership::Owned, InferredOwnership::Borrowed),
1197 (ClassificationMethod::Hybrid, InferredOwnership::Vec, InferredOwnership::Vec),
1198 ] {
1199 let result = HybridResult {
1200 variable: "x".to_string(),
1201 ownership: rule_own,
1202 confidence: 0.8,
1203 method,
1204 rule_result: Some(rule_own),
1205 ml_result: Some(OwnershipPrediction {
1206 kind: ml_own,
1207 confidence: 0.8,
1208 fallback: None,
1209 }),
1210 reasoning: "test".to_string(),
1211 };
1212 metrics.record(&result);
1213 }
1214
1215 assert_eq!(metrics.total, 4);
1216 assert_eq!(metrics.rule_based, 1);
1217 assert_eq!(metrics.ml_used, 1);
1218 assert_eq!(metrics.fallback, 1);
1219 assert_eq!(metrics.hybrid, 1);
1220 assert_eq!(metrics.agreements, 3);
1223 assert_eq!(metrics.disagreements, 1);
1224 }
1225
1226 #[test]
1227 fn hybrid_result_ml_not_rejected_without_ml_result() {
1228 let result = HybridResult {
1229 variable: "x".to_string(),
1230 ownership: InferredOwnership::Owned,
1231 confidence: 0.8,
1232 method: ClassificationMethod::Fallback,
1233 rule_result: Some(InferredOwnership::Owned),
1234 ml_result: None, reasoning: "rules only".to_string(),
1236 };
1237
1238 assert!(result.used_fallback());
1239 assert!(!result.ml_rejected());
1241 }
1242
1243 #[test]
1248 fn null_model_batch_predict() {
1249 let model = NullModel;
1250 let features = vec![
1251 OwnershipFeatures::default(),
1252 OwnershipFeatures::default(),
1253 OwnershipFeatures::default(),
1254 ];
1255 let predictions = model.predict_batch(&features);
1256 assert_eq!(predictions.len(), 3);
1257 for pred in &predictions {
1258 assert_eq!(pred.kind, InferredOwnership::RawPointer);
1259 assert!((pred.confidence as f64).abs() < 0.001);
1260 }
1261 }
1262
1263 struct AnonymousModel;
1269
1270 impl OwnershipModel for AnonymousModel {
1271 fn predict(&self, _features: &OwnershipFeatures) -> OwnershipPrediction {
1272 OwnershipPrediction {
1273 kind: InferredOwnership::Owned,
1274 confidence: 0.5,
1275 fallback: None,
1276 }
1277 }
1278 }
1280
1281 #[test]
1282 fn ownership_model_default_name() {
1283 let model = AnonymousModel;
1284 assert_eq!(model.name(), "unknown");
1285 }
1286
1287 #[test]
1288 fn ownership_model_default_predict_batch() {
1289 let model = AnonymousModel;
1290 let features = vec![OwnershipFeatures::default(); 2];
1291 let predictions = model.predict_batch(&features);
1292 assert_eq!(predictions.len(), 2);
1293 assert_eq!(predictions[0].kind, InferredOwnership::Owned);
1294 }
1295
1296 #[test]
1297 fn hybrid_classifier_default_impl() {
1298 let classifier = HybridClassifier::default();
1299 assert!(!classifier.ml_enabled());
1300 assert!((classifier.confidence_threshold() - DEFAULT_CONFIDENCE_THRESHOLD).abs() < 0.001);
1301 }
1302}