## Coverage Report

#### Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/ruchy/src/quality/scoring.rs

    Line
    Count
    Source
    1
    //! Unified quality scoring system for Ruchy code (RUCHY-0810)
    2
    //! Incremental scoring architecture (RUCHY-0813)
    3
    4
    use std::collections::HashMap;
    5
    use std::time::{Duration, SystemTime};
    6
    use std::path::PathBuf;
    7
    use std::fs;
    8
    use crate::frontend::ast::ExprKind;
    9
    10
    /// Analysis depth for quality scoring
    11
    #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
    12
    pub enum AnalysisDepth {
    13
        /// <100ms - AST metrics only
    14
        Shallow,
    15
        /// <1s - AST + type checking + basic flow
    16
        Standard,
    17
        /// <30s - Full property/mutation testing
    18
        Deep,
    19
    }
    20
    21
    /// Unified quality score with components
    22
    #[derive(Debug, Clone)]
    23
    pub struct QualityScore {
    24
        pub value: f64,           // 0.0-1.0 normalized score
    25
        pub components: ScoreComponents,
    26
        pub grade: Grade,         // Human-readable grade
    27
        pub confidence: f64,      // Confidence in score accuracy
    28
        pub cache_hit_rate: f64,  // Percentage from cached analysis
    29
    }
    30
    31
    /// Individual score components
    32
    #[derive(Debug, Clone)]
    33
    pub struct ScoreComponents {
    34
        pub correctness: f64,     // 35% - Semantic correctness
    35
        pub performance: f64,     // 25% - Runtime efficiency
    36
        pub maintainability: f64, // 20% - Change resilience
    37
        pub safety: f64,          // 15% - Memory/type safety
    38
        pub idiomaticity: f64,    // 5%  - Language conventions
    39
    }
    40
    41
    /// Human-readable grade boundaries
    42
    #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
    43
    pub enum Grade {
    44
        APlus,  // [0.97, 1.00] - Ship to production
    45
        A,      // [0.93, 0.97) - Ship with confidence
    46
        AMinus, // [0.90, 0.93) - Ship with review
    47
        BPlus,  // [0.87, 0.90) - Acceptable
    48
        B,      // [0.83, 0.87) - Needs work
    49
        BMinus, // [0.80, 0.83) - Minimum viable
    50
        CPlus,  // [0.77, 0.80) - Technical debt
    51
        C,      // [0.73, 0.77) - Refactor advised
    52
        CMinus, // [0.70, 0.73) - Refactor required
    53
        D,      // [0.60, 0.70) - Major issues
    54
        F,      // [0.00, 0.60) - Fundamental problems
    55
    }
    56
    57
    impl Grade {
    58
    0
        pub fn from_score(value: f64) -> Self {
    59
    0
            match value {
    60
    0
                v if v >= 0.97 => Grade::APlus,
    61
    0
                v if v >= 0.93 => Grade::A,
    62
    0
                v if v >= 0.90 => Grade::AMinus,
    63
    0
                v if v >= 0.87 => Grade::BPlus,
    64
    0
                v if v >= 0.83 => Grade::B,
    65
    0
                v if v >= 0.80 => Grade::BMinus,
    66
    0
                v if v >= 0.77 => Grade::CPlus,
    67
    0
                v if v >= 0.73 => Grade::C,
    68
    0
                v if v >= 0.70 => Grade::CMinus,
    69
    0
                v if v >= 0.60 => Grade::D,
    70
    0
                _ => Grade::F,
    71
            }
    72
    0
        }
    73
    }
    74
    75
    impl std::fmt::Display for Grade {
    76
    0
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    77
    0
            match self {
    78
    0
                Grade::APlus => write!(f, "A+"),
    79
    0
                Grade::A => write!(f, "A"),
    80
    0
                Grade::AMinus => write!(f, "A-"),
    81
    0
                Grade::BPlus => write!(f, "B+"),
    82
    0
                Grade::B => write!(f, "B"),
    83
    0
                Grade::BMinus => write!(f, "B-"),
    84
    0
                Grade::CPlus => write!(f, "C+"),
    85
    0
                Grade::C => write!(f, "C"),
    86
    0
                Grade::CMinus => write!(f, "C-"),
    87
    0
                Grade::D => write!(f, "D"),
    88
    0
                Grade::F => write!(f, "F"),
    89
            }
    90
    0
        }
    91
    }
    92
    93
    /// Configuration for score weights
    94
    #[derive(Debug, Clone)]
    95
    pub struct ScoreConfig {
    96
        pub correctness_weight: f64,
    97
        pub performance_weight: f64,
    98
        pub maintainability_weight: f64,
    99
        pub safety_weight: f64,
    100
        pub idiomaticity_weight: f64,
    101
    }
    102
    103
    impl Default for ScoreConfig {
    104
    0
        fn default() -> Self {
    105
    0
            Self {
    106
    0
                correctness_weight: 0.35,
    107
    0
                performance_weight: 0.25,
    108
    0
                maintainability_weight: 0.20,
    109
    0
                safety_weight: 0.15,
    110
    0
                idiomaticity_weight: 0.05,
    111
    0
            }
    112
    0
        }
    113
    }
    114
    115
    /// Cache key for scoring results
    116
    #[derive(Debug, Clone, Hash, PartialEq, Eq)]
    117
    pub struct CacheKey {
    118
        pub file_path: PathBuf,
    119
        pub content_hash: u64,
    120
        pub depth: AnalysisDepth,
    121
    }
    122
    123
    /// Cached scoring result with metadata
    124
    #[derive(Debug, Clone)]
    125
    pub struct CacheEntry {
    126
        pub score: QualityScore,
    127
        pub timestamp: SystemTime,
    128
        pub dependencies: Vec<PathBuf>,
    129
    }
    130
    131
    /// File dependency tracker
    132
    #[derive(Debug)]
    133
    pub struct DependencyTracker {
    134
        /// Map of file -> files it depends on
    135
        dependencies: HashMap<PathBuf, Vec<PathBuf>>,
    136
        /// Map of file -> last modified time
    137
        file_times: HashMap<PathBuf, SystemTime>,
    138
    }
    139
    140
    impl Default for DependencyTracker {
    141
    0
        fn default() -> Self {
    142
    0
            Self::new()
    143
    0
        }
    144
    }
    145
    146
    impl DependencyTracker {
    147
    0
        pub fn new() -> Self {
    148
    0
            Self {
    149
    0
                dependencies: HashMap::new(),
    150
    0
                file_times: HashMap::new(),
    151
    0
            }
    152
    0
        }
    153
    154
    0
        pub fn track_dependency(&mut self, file: PathBuf, dependency: PathBuf) {
    155
    0
            self.dependencies.entry(file).or_default().push(dependency);
    156
    0
        }
    157
    158
    0
        pub fn is_stale(&self, file: &PathBuf) -> bool {
    159
    0
            if let Some(dependencies) = self.dependencies.get(file) {
    160
    0
                for dep in dependencies {
    161
    0
                    if self.is_file_modified(dep) {
    162
    0
                        return true;
    163
    0
                    }
    164
                }
    165
    0
            }
    166
    0
            false
    167
    0
        }
    168
    169
    0
        fn is_file_modified(&self, file: &PathBuf) -> bool {
    170
    0
            let Ok(metadata) = fs::metadata(file) else { return true; };
    171
    0
            let Ok(modified) = metadata.modified() else { return true; };
    172
    173
    0
            if let Some(&cached_time) = self.file_times.get(file) {
    174
    0
                modified > cached_time
    175
            } else {
    176
    0
                true
    177
            }
    178
    0
        }
    179
    180
    0
        pub fn update_file_time(&mut self, file: PathBuf) {
    181
    0
            if let Ok(metadata) = fs::metadata(&file) {
    182
    0
                if let Ok(modified) = metadata.modified() {
    183
    0
                    self.file_times.insert(file, modified);
    184
    0
                }
    185
    0
            }
    186
    0
        }
    187
    }
    188
    189
    /// Incremental scoring engine with caching
    190
    pub struct ScoreEngine {
    191
        config: ScoreConfig,
    192
        cache: HashMap<CacheKey, CacheEntry>,
    193
        dependency_tracker: DependencyTracker,
    194
    }
    195
    196
    impl ScoreEngine {
    197
    0
        pub fn new(config: ScoreConfig) -> Self {
    198
    0
            Self {
    199
    0
                config,
    200
    0
                cache: HashMap::new(),
    201
    0
                dependency_tracker: DependencyTracker::new(),
    202
    0
            }
    203
    0
        }
    204
    205
    0
        pub fn score(&self, ast: &crate::frontend::ast::Expr, depth: AnalysisDepth) -> QualityScore {
    206
    0
            let components = match depth {
    207
    0
                AnalysisDepth::Shallow => Self::score_shallow(ast),
    208
    0
                AnalysisDepth::Standard => Self::score_standard(ast),
    209
    0
                AnalysisDepth::Deep => Self::score_deep(ast),
    210
            };
    211
    212
    0
            let value = self.calculate_weighted_score(&components);
    213
    0
            let grade = Grade::from_score(value);
    214
    0
            let confidence = Self::calculate_confidence(depth);
    215
    216
    0
            QualityScore {
    217
    0
                value,
    218
    0
                components,
    219
    0
                grade,
    220
    0
                confidence,
    221
    0
                cache_hit_rate: 0.0, // No file path in legacy API
    222
    0
            }
    223
    0
        }
    224
    225
        /// Incremental scoring with file-based caching (RUCHY-0813)
    226
    0
        pub fn score_incremental(
    227
    0
            &mut self,
    228
    0
            ast: &crate::frontend::ast::Expr,
    229
    0
            file_path: PathBuf,
    230
    0
            content: &str,
    231
    0
            depth: AnalysisDepth,
    232
    0
        ) -> QualityScore {
    233
    0
            let content_hash = Self::hash_content(content);
    234
    0
            let cache_key = CacheKey {
    235
    0
                file_path: file_path.clone(),
    236
    0
                content_hash,
    237
    0
                depth,
    238
    0
            };
    239
    240
            // Check cache first
    241
    0
            if let Some(entry) = self.cache.get(&cache_key) {
    242
    0
                if !self.dependency_tracker.is_stale(&file_path) {
    243
    0
                    let mut score = entry.score.clone();
    244
    0
                    score.cache_hit_rate = 1.0;
    245
    0
                    return score;
    246
    0
                }
    247
    0
            }
    248
    249
            // Fast path for small files - skip complex analysis
    250
    0
            let start = std::time::Instant::now();
    251
    0
            let is_small_file = content.len() < 1024;
    252
    0
            let effective_depth = if is_small_file && depth != AnalysisDepth::Deep {
    253
    0
                AnalysisDepth::Shallow
    254
            } else {
    255
    0
                depth
    256
            };
    257
    258
    0
            let components = match effective_depth {
    259
    0
                AnalysisDepth::Shallow => Self::score_shallow(ast),
    260
    0
                AnalysisDepth::Standard => Self::score_standard(ast),
    261
    0
                AnalysisDepth::Deep => Self::score_deep(ast),
    262
            };
    263
    264
    0
            let value = self.calculate_weighted_score(&components);
    265
    0
            let grade = Grade::from_score(value);
    266
    0
            let confidence = if is_small_file && depth != effective_depth {
    267
    0
                Self::calculate_confidence(effective_depth) * 0.9 // Slightly reduced confidence for fast path
    268
            } else {
    269
    0
                Self::calculate_confidence(depth)
    270
            };
    271
    0
            let elapsed = start.elapsed();
    272
    273
    0
            let score = QualityScore {
    274
    0
                value,
    275
    0
                components,
    276
    0
                grade,
    277
    0
                confidence,
    278
    0
                cache_hit_rate: 0.0,
    279
    0
            };
    280
    281
            // Cache the result only if worth caching
    282
    0
            let is_worth_caching = elapsed > Duration::from_millis(10) || !is_small_file;
    283
    0
            if is_worth_caching {
    284
    0
                let entry = CacheEntry {
    285
    0
                    score: score.clone(),
    286
    0
                    timestamp: SystemTime::now(),
    287
    0
                    dependencies: Self::extract_dependencies(ast),
    288
    0
                };
    289
    0
                self.cache.insert(cache_key, entry);
    290
    0
            }
    291
    292
    0
            self.dependency_tracker.update_file_time(file_path);
    293
    294
            // Maintain cache if scoring took too long or cache is getting large
    295
    0
            if elapsed > Duration::from_millis(100) || self.cache.len() > 1000 {
    296
    0
                self.optimize_cache();
    297
    0
            }
    298
    299
    0
            score
    300
    0
        }
    301
    302
        /// Progressive scoring that refines analysis depth based on time budget
    303
    0
        pub fn score_progressive(
    304
    0
            &mut self,
    305
    0
            ast: &crate::frontend::ast::Expr,
    306
    0
            file_path: PathBuf,
    307
    0
            content: &str,
    308
    0
            time_budget: Duration,
    309
    0
        ) -> QualityScore {
    310
    0
            let start = std::time::Instant::now();
    311
    312
            // Start with shallow analysis
    313
    0
            let mut score = self.score_incremental(ast, file_path.clone(), content, AnalysisDepth::Shallow);
    314
    315
    0
            if start.elapsed() < time_budget / 3 {
    316
                // Upgrade to standard analysis
    317
    0
                score = self.score_incremental(ast, file_path.clone(), content, AnalysisDepth::Standard);
    318
    319
    0
                if start.elapsed() < time_budget * 2 / 3 {
    320
    0
                    // Upgrade to deep analysis
    321
    0
                    score = self.score_incremental(ast, file_path, content, AnalysisDepth::Deep);
    322
    0
                }
    323
    0
            }
    324
    325
    0
            score
    326
    0
        }
    327
    328
    0
        fn hash_content(content: &str) -> u64 {
    329
            use std::collections::hash_map::DefaultHasher;
    330
            use std::hash::{Hash, Hasher};
    331
    332
    0
            let mut hasher = DefaultHasher::new();
    333
    0
            content.hash(&mut hasher);
    334
    0
            hasher.finish()
    335
    0
        }
    336
    337
    0
        fn extract_dependencies(_ast: &crate::frontend::ast::Expr) -> Vec<PathBuf> {
    338
            // Extract import/use dependencies from AST
    339
            // Implementation in RUCHY-0814 with type checker integration
    340
    0
            Vec::new()
    341
    0
        }
    342
    343
    0
        fn optimize_cache(&mut self) {
    344
            // Remove old cache entries to maintain <100ms performance
    345
    0
            let now = SystemTime::now();
    346
    0
            let cutoff = Duration::from_secs(300); // 5 minutes
    347
    0
            let max_entries = 500; // Maximum cache entries for performance
    348
    349
            // First pass: Remove old entries
    350
    0
            self.cache.retain(|_, entry| {
    351
    0
                if let Ok(age) = now.duration_since(entry.timestamp) {
    352
    0
                    age < cutoff
    353
                } else {
    354
    0
                    false
    355
                }
    356
    0
            });
    357
    358
            // Second pass: If still too many entries, remove least recently used
    359
    0
            if self.cache.len() > max_entries {
    360
    0
                let mut entries: Vec<_> = self.cache.iter().map(|(k, v)| (k.clone(), v.timestamp)).collect();
    361
    0
                entries.sort_by_key(|(_, timestamp)| *timestamp);
    362
    363
    0
                let to_remove = self.cache.len() - max_entries;
    364
    0
                let keys_to_remove: Vec<_> = entries.iter()
    365
    0
                    .take(to_remove)
    366
    0
                    .map(|(k, _)| k.clone())
    367
    0
                    .collect();
    368
    369
    0
                for key in keys_to_remove {
    370
    0
                    self.cache.remove(&key);
    371
    0
                }
    372
    0
            }
    373
    0
        }
    374
    375
        /// Clear all caches - useful for memory management
    376
    0
        pub fn clear_cache(&mut self) {
    377
    0
            self.cache.clear();
    378
    0
            self.dependency_tracker = DependencyTracker::new();
    379
    0
        }
    380
    381
        /// Get cache statistics
    382
    0
        pub fn cache_stats(&self) -> CacheStats {
    383
    0
            CacheStats {
    384
    0
                entries: self.cache.len(),
    385
    0
                memory_usage_estimate: self.cache.len() * 1024, // Rough estimate
    386
    0
            }
    387
    0
        }
    388
    389
    0
        fn score_shallow(ast: &crate::frontend::ast::Expr) -> ScoreComponents {
    390
            // Fast AST-only analysis (<100ms)
    391
    0
            let metrics = analyze_ast_metrics(ast);
    392
    393
    0
            let correctness = 1.0;
    394
    0
            let mut performance = 1.0;
    395
    0
            let mut maintainability = 1.0;
    396
    0
            let safety = 1.0;
    397
    0
            let idiomaticity = 1.0;
    398
    399
            // Penalize high complexity
    400
    0
            if metrics.max_depth > 10 {
    401
    0
                maintainability *= 0.9;
    402
    0
            }
    403
    0
            if metrics.function_count > 50 {
    404
    0
                maintainability *= 0.95;
    405
    0
            }
    406
    407
            // Penalize deep nesting
    408
    0
            if metrics.max_nesting > 5 {
    409
    0
                performance *= 0.9;
    410
    0
                maintainability *= 0.9;
    411
    0
            }
    412
    413
            // Penalize excessive lines
    414
    0
            if metrics.line_count > 1000 {
    415
    0
                maintainability *= 0.95;
    416
    0
            }
    417
    418
    0
            ScoreComponents {
    419
    0
                correctness,
    420
    0
                performance,
    421
    0
                maintainability,
    422
    0
                safety,
    423
    0
                idiomaticity,
    424
    0
            }
    425
    0
        }
    426
    427
    0
        fn score_standard(ast: &crate::frontend::ast::Expr) -> ScoreComponents {
    428
            // Standard analysis with type checking (<1s)
    429
    0
            let mut components = Self::score_shallow(ast);
    430
    431
            // Additional type-based analysis
    432
            // Type checker integration in RUCHY-0814
    433
    0
            components.correctness *= 0.95;
    434
    0
            components.safety *= 0.95;
    435
    436
    0
            components
    437
    0
        }
    438
    439
    0
        fn score_deep(ast: &crate::frontend::ast::Expr) -> ScoreComponents {
    440
            // Deep analysis with property testing (<30s)
    441
    0
            let mut components = Self::score_standard(ast);
    442
    443
            // Additional deep analysis
    444
            // Property testing and mutation testing in RUCHY-0816
    445
    0
            components.correctness *= 0.98;
    446
    447
    0
            components
    448
    0
        }
    449
    450
    0
        fn calculate_weighted_score(&self, components: &ScoreComponents) -> f64 {
    451
    0
            components.correctness * self.config.correctness_weight
    452
    0
                + components.performance * self.config.performance_weight
    453
    0
                + components.maintainability * self.config.maintainability_weight
    454
    0
                + components.safety * self.config.safety_weight
    455
    0
                + components.idiomaticity * self.config.idiomaticity_weight
    456
    0
        }
    457
    458
    0
        fn calculate_confidence(depth: AnalysisDepth) -> f64 {
    459
    0
            match depth {
    460
    0
                AnalysisDepth::Shallow => 0.6,
    461
    0
                AnalysisDepth::Standard => 0.8,
    462
    0
                AnalysisDepth::Deep => 0.95,
    463
            }
    464
    0
        }
    465
    }
    466
    467
    /// AST metrics for analysis
    468
    #[derive(Debug)]
    469
    struct AstMetrics {
    470
        function_count: usize,
    471
        max_depth: usize,
    472
        max_nesting: usize,
    473
        line_count: usize,
    474
        cyclomatic_complexity: usize,
    475
    }
    476
    477
    0
    fn analyze_ast_metrics(ast: &crate::frontend::ast::Expr) -> AstMetrics {
    478
    0
        let mut metrics = AstMetrics {
    479
    0
            function_count: 0,
    480
    0
            max_depth: 0,
    481
    0
            max_nesting: 0,
    482
    0
            line_count: 0,
    483
    0
            cyclomatic_complexity: 1, // Base complexity
    484
    0
        };
    485
    486
    0
        analyze_expr(ast, &mut metrics, 0, 0);
    487
    0
        metrics
    488
    0
    }
    489
    490
    0
    fn analyze_expr(expr: &crate::frontend::ast::Expr, metrics: &mut AstMetrics, depth: usize, nesting: usize) {
    491
    492
    0
        metrics.max_depth = metrics.max_depth.max(depth);
    493
    0
        metrics.max_nesting = metrics.max_nesting.max(nesting);
    494
    495
    0
        match &expr.kind {
    496
    0
            ExprKind::Function { body, .. } => {
    497
    0
                metrics.function_count += 1;
    498
    0
                analyze_expr(body, metrics, depth + 1, 0);
    499
    0
            }
    500
    0
            ExprKind::Block(exprs) => {
    501
    0
                for e in exprs {
    502
    0
                    analyze_expr(e, metrics, depth + 1, nesting);
    503
    0
                }
    504
            }
    505
    0
            ExprKind::If { condition, then_branch, else_branch } => {
    506
    0
                metrics.cyclomatic_complexity += 1;
    507
    0
                analyze_expr(condition, metrics, depth + 1, nesting + 1);
    508
    0
                analyze_expr(then_branch, metrics, depth + 1, nesting + 1);
    509
    0
                if let Some(else_expr) = else_branch {
    510
    0
                    analyze_expr(else_expr, metrics, depth + 1, nesting + 1);
    511
    0
                }
    512
            }
    513
    0
            ExprKind::While { condition, body } => {
    514
    0
                metrics.cyclomatic_complexity += 1;
    515
    0
                analyze_expr(condition, metrics, depth + 1, nesting + 1);
    516
    0
                analyze_expr(body, metrics, depth + 1, nesting + 1);
    517
    0
            }
    518
    0
            ExprKind::For { iter, body, .. } => {
    519
    0
                metrics.cyclomatic_complexity += 1;
    520
    0
                analyze_expr(iter, metrics, depth + 1, nesting + 1);
    521
    0
                analyze_expr(body, metrics, depth + 1, nesting + 1);
    522
    0
            }
    523
    0
            ExprKind::Match { expr: match_expr, arms } => {
    524
    0
                analyze_expr(match_expr, metrics, depth + 1, nesting);
    525
    0
                for arm in arms {
    526
    0
                    metrics.cyclomatic_complexity += 1;
    527
    0
                    // Guards have been removed from the grammar
    528
    0
                    analyze_expr(&arm.body, metrics, depth + 1, nesting + 1);
    529
    0
                }
    530
            }
    531
    0
            _ => {}
    532
        }
    533
    0
    }
    534
    535
    impl QualityScore {
    536
        /// Explain changes from a baseline score
    537
    0
        pub fn explain_delta(&self, baseline: &QualityScore) -> ScoreExplanation {
    538
    0
            let delta = self.value - baseline.value;
    539
    0
            let mut changes = Vec::new();
    540
    0
            let mut tradeoffs = Vec::new();
    541
    542
            // Track component changes
    543
    0
            let components = [
    544
    0
                ("Correctness", self.components.correctness, baseline.components.correctness),
    545
    0
                ("Performance", self.components.performance, baseline.components.performance),
    546
    0
                ("Maintainability", self.components.maintainability, baseline.components.maintainability),
    547
    0
                ("Safety", self.components.safety, baseline.components.safety),
    548
    0
                ("Idiomaticity", self.components.idiomaticity, baseline.components.idiomaticity),
    549
    0
            ];
    550
    551
    0
            for (name, current, baseline) in components {
    552
    0
                let diff = current - baseline;
    553
    0
                if diff.abs() > 0.01 {
    554
    0
                    changes.push(format!("{}: {}{:.1}%",
    555
                        name,
    556
    0
                        if diff > 0.0 { "+" } else { "" },
    557
    0
                        diff * 100.0
    558
                    ));
    559
    0
                }
    560
            }
    561
    562
            // Detect tradeoffs
    563
    0
            if self.components.performance > baseline.components.performance
    564
    0
                && self.components.maintainability < baseline.components.maintainability {
    565
    0
                tradeoffs.push("Performance improved at the cost of maintainability".to_string());
    566
    0
            }
    567
    568
    0
            if self.components.safety > baseline.components.safety
    569
    0
                && self.components.performance < baseline.components.performance {
    570
    0
                tradeoffs.push("Safety improved at the cost of performance".to_string());
    571
    0
            }
    572
    573
    0
            ScoreExplanation {
    574
    0
                delta,
    575
    0
                changes,
    576
    0
                tradeoffs,
    577
    0
                grade_change: format!("{} → {}", baseline.grade, self.grade),
    578
    0
            }
    579
    0
        }
    580
    }
    581
    582
    /// Explanation of score changes
    583
    pub struct ScoreExplanation {
    584
        pub delta: f64,
    585
        pub changes: Vec<String>,
    586
        pub tradeoffs: Vec<String>,
    587
        pub grade_change: String,
    588
    }
    589
    590
    /// Cache performance statistics
    591
    #[derive(Debug, Clone)]
    592
    pub struct CacheStats {
    593
        pub entries: usize,
    594
        pub memory_usage_estimate: usize,
    595
    }
    596
    597
    /// Score correctness component (35% weight)
    598
    0
    pub fn score_correctness(ast: &crate::frontend::ast::Expr) -> f64 {
    599
    0
        let mut score = 1.0;
    600
    601
        // Pattern match exhaustiveness check
    602
    0
        let pattern_completeness = analyze_pattern_completeness(ast);
    603
    0
        score *= pattern_completeness;
    604
    605
        // Error handling coverage
    606
    0
        let error_handling_quality = analyze_error_handling(ast);
    607
    0
        score *= error_handling_quality;
    608
    609
        // Type consistency analysis
    610
    0
        let type_consistency = analyze_type_consistency(ast);
    611
    0
        score *= type_consistency;
    612
    613
        // Logical soundness (basic checks)
    614
    0
        let logical_soundness = analyze_logical_soundness(ast);
    615
    0
        score *= logical_soundness;
    616
    617
    0
        score.clamp(0.0, 1.0)
    618
    0
    }
    619
    620
    /// Analyze pattern match completeness
    621
    0
    fn analyze_pattern_completeness(ast: &crate::frontend::ast::Expr) -> f64 {
    622
    0
        let mut total_matches = 0;
    623
    0
        let mut complete_matches = 0;
    624
    625
    0
        analyze_pattern_completeness_recursive(ast, &mut total_matches, &mut complete_matches);
    626
    627
    0
        if total_matches == 0 {
    628
    0
            1.0 // No matches, assume complete
    629
        } else {
    630
            #[allow(clippy::cast_precision_loss)]
    631
    0
            let score = (complete_matches as f64) / (total_matches as f64);
    632
    0
            score
    633
        }
    634
    0
    }
    635
    636
    0
    fn analyze_pattern_completeness_recursive(
    637
    0
        expr: &crate::frontend::ast::Expr,
    638
    0
        total_matches: &mut usize,
    639
    0
        complete_matches: &mut usize,
    640
    0
    ) {
    641
    642
    0
        match &expr.kind {
    643
    0
            ExprKind::Match { expr: match_expr, arms } => {
    644
    0
                *total_matches += 1;
    645
    646
                // Check if match has wildcard pattern or covers all cases
    647
    0
                let has_wildcard = arms.iter().any(|arm| {
    648
    0
                    matches!(arm.pattern, crate::frontend::ast::Pattern::Wildcard)
    649
    0
                });
    650
    651
    0
                if has_wildcard || arms.len() >= 2 { // Basic heuristic
    652
    0
                    *complete_matches += 1;
    653
    0
                }
    654
    655
    0
                analyze_pattern_completeness_recursive(match_expr, total_matches, complete_matches);
    656
    0
                for arm in arms {
    657
    0
                    analyze_pattern_completeness_recursive(&arm.body, total_matches, complete_matches);
    658
    0
                }
    659
            }
    660
    0
            ExprKind::If { condition, then_branch, else_branch } => {
    661
    0
                analyze_pattern_completeness_recursive(condition, total_matches, complete_matches);
    662
    0
                analyze_pattern_completeness_recursive(then_branch, total_matches, complete_matches);
    663
    0
                if let Some(else_expr) = else_branch {
    664
    0
                    analyze_pattern_completeness_recursive(else_expr, total_matches, complete_matches);
    665
    0
                }
    666
            }
    667
    0
            ExprKind::Block(exprs) => {
    668
    0
                for e in exprs {
    669
    0
                    analyze_pattern_completeness_recursive(e, total_matches, complete_matches);
    670
    0
                }
    671
            }
    672
    0
            ExprKind::Function { body, .. } => {
    673
    0
                analyze_pattern_completeness_recursive(body, total_matches, complete_matches);
    674
    0
            }
    675
    0
            _ => {}
    676
        }
    677
    0
    }
    678
    679
    /// Analyze error handling quality
    680
    0
    fn analyze_error_handling(ast: &crate::frontend::ast::Expr) -> f64 {
    681
    0
        let mut total_fallible_ops = 0;
    682
    0
        let mut handled_ops = 0;
    683
    684
    0
        analyze_error_handling_recursive(ast, &mut total_fallible_ops, &mut handled_ops);
    685
    686
    0
        if total_fallible_ops == 0 {
    687
    0
            1.0 // No fallible operations
    688
        } else {
    689
            #[allow(clippy::cast_precision_loss)]
    690
    0
            let base_score = (handled_ops as f64) / (total_fallible_ops as f64);
    691
            // Boost score if some error handling is present
    692
    0
            if handled_ops > 0 {
    693
    0
                (base_score + 0.3).min(1.0)
    694
            } else {
    695
    0
                0.7 // Penalty for no error handling
    696
            }
    697
        }
    698
    0
    }
    699
    700
    0
    fn analyze_error_handling_recursive(
    701
    0
        expr: &crate::frontend::ast::Expr,
    702
    0
        total_fallible_ops: &mut usize,
    703
    0
        handled_ops: &mut usize,
    704
    0
    ) {
    705
    706
    0
        match &expr.kind {
    707
    0
            ExprKind::Match { expr: match_expr, arms } => {
    708
                // Check if matching on Result type (heuristic)
    709
    0
                if arms.len() >= 2 {
    710
    0
                    *total_fallible_ops += 1;
    711
    0
                    *handled_ops += 1;
    712
    0
                }
    713
    0
                analyze_error_handling_recursive(match_expr, total_fallible_ops, handled_ops);
    714
    0
                for arm in arms {
    715
    0
                    analyze_error_handling_recursive(&arm.body, total_fallible_ops, handled_ops);
    716
    0
                }
    717
            }
    718
    0
            ExprKind::Block(exprs) => {
    719
    0
                for e in exprs {
    720
    0
                    analyze_error_handling_recursive(e, total_fallible_ops, handled_ops);
    721
    0
                }
    722
            }
    723
    0
            ExprKind::Function { body, .. } => {
    724
    0
                analyze_error_handling_recursive(body, total_fallible_ops, handled_ops);
    725
    0
            }
    726
    0
            _ => {}
    727
        }
    728
    0
    }
    729
    730
    /// Analyze type consistency
    731
    0
    fn analyze_type_consistency(_ast: &crate::frontend::ast::Expr) -> f64 {
    732
        // For now, assume good consistency since we have type checking
    733
        // Future: integrate with type checker for real analysis
    734
    0
        0.95
    735
    0
    }
    736
    737
    /// Analyze logical soundness
    738
    0
    fn analyze_logical_soundness(ast: &crate::frontend::ast::Expr) -> f64 {
    739
    0
        let mut score = 1.0;
    740
    741
        // Check for obvious logical issues
    742
    0
        let has_unreachable = has_unreachable_code(ast);
    743
    0
        if has_unreachable {
    744
    0
            score *= 0.8; // Penalty for unreachable code
    745
    0
        }
    746
    747
    0
        let has_infinite_loops = has_potential_infinite_loops(ast);
    748
    0
        if has_infinite_loops {
    749
    0
            score *= 0.9; // Penalty for potential infinite loops
    750
    0
        }
    751
    752
    0
        score
    753
    0
    }
    754
    755
    0
    fn has_unreachable_code(ast: &crate::frontend::ast::Expr) -> bool {
    756
    757
    0
        match &ast.kind {
    758
    0
            ExprKind::Block(exprs) => {
    759
    0
                for (i, expr) in exprs.iter().enumerate() {
    760
    0
                    if i < exprs.len() - 1 && is_diverging_expr(expr) {
    761
    0
                        return true; // Code after diverging expression
    762
    0
                    }
    763
    0
                    if has_unreachable_code(expr) {
    764
    0
                        return true;
    765
    0
                    }
    766
                }
    767
    0
                false
    768
            }
    769
    0
            ExprKind::If { condition, then_branch, else_branch } => {
    770
    0
                has_unreachable_code(condition) ||
    771
    0
                has_unreachable_code(then_branch) ||
    772
    0
                else_branch.as_ref().is_some_and(|e| has_unreachable_code(e))
    773
            }
    774
    0
            _ => false
    775
        }
    776
    0
    }
    777
    778
    0
    fn is_diverging_expr(expr: &crate::frontend::ast::Expr) -> bool {
    779
    780
    0
        match &expr.kind {
    781
    0
            ExprKind::Call { func, .. } => {
    782
                // Check for known diverging functions (heuristic)
    783
    0
                if let ExprKind::Identifier(name) = &func.kind {
    784
    0
                    matches!(name.as_str(), "panic" | "unreachable" | "exit")
    785
                } else {
    786
    0
                    false
    787
                }
    788
            }
    789
    0
            _ => false
    790
        }
    791
    0
    }
    792
    793
    0
    fn has_potential_infinite_loops(ast: &crate::frontend::ast::Expr) -> bool {
    794
    795
    0
        match &ast.kind {
    796
    0
            ExprKind::While { condition, body } => {
    797
                // Check for trivial infinite loops: while true { ... }
    798
    0
                if let ExprKind::Literal(crate::frontend::ast::Literal::Bool(true)) = &condition.kind {
    799
                    // Check if body has break statement
    800
    0
                    !has_break_statement(body)
    801
                } else {
    802
    0
                    has_potential_infinite_loops(condition) || has_potential_infinite_loops(body)
    803
                }
    804
            }
    805
    0
            ExprKind::Block(exprs) => exprs.iter().any(has_potential_infinite_loops),
    806
    0
            ExprKind::Function { body, .. } => has_potential_infinite_loops(body),
    807
    0
            _ => false
    808
        }
    809
    0
    }
    810
    811
    0
    fn has_break_statement(ast: &crate::frontend::ast::Expr) -> bool {
    812
    813
    0
        match &ast.kind {
    814
    0
            ExprKind::Break { .. } => true,
    815
    0
            ExprKind::Block(exprs) => exprs.iter().any(has_break_statement),
    816
    0
            ExprKind::If { condition, then_branch, else_branch } => {
    817
    0
                has_break_statement(condition) ||
    818
    0
                has_break_statement(then_branch) ||
    819
    0
                else_branch.as_ref().is_some_and(|e| has_break_statement(e))
    820
            }
    821
    0
            _ => false
    822
        }
    823
    0
    }
    824
    825
    /// Score performance component (25% weight)
    826
    0
    pub fn score_performance(ast: &crate::frontend::ast::Expr) -> f64 {
    827
    0
        let metrics = analyze_ast_metrics(ast);
    828
    0
        let mut score = 1.0;
    829
    830
        // Complexity analysis (BigO implications)
    831
    0
        let complexity_score = analyze_algorithmic_complexity(ast);
    832
    0
        score *= complexity_score;
    833
    834
        // Penalize high cyclomatic complexity (affects branch prediction)
    835
    0
        if metrics.cyclomatic_complexity > 10 {
    836
    0
            #[allow(clippy::cast_precision_loss)]
    837
    0
            let penalty = ((metrics.cyclomatic_complexity - 10) as f64 * 0.02).min(0.3);
    838
    0
            score *= 1.0 - penalty;
    839
    0
        }
    840
    841
        // Penalize deep nesting (affects cache performance)
    842
    0
        if metrics.max_nesting > 3 {
    843
    0
            #[allow(clippy::cast_precision_loss)]
    844
    0
            let penalty = ((metrics.max_nesting - 3) as f64 * 0.05).min(0.2);
    845
    0
            score *= 1.0 - penalty;
    846
    0
        }
    847
    848
        // Allocation analysis
    849
    0
        let allocation_score = analyze_allocation_patterns(ast);
    850
    0
        score *= allocation_score;
    851
    852
        // Memory access patterns (simplified for current AST)
    853
        // Future enhancement when Index/Dict variants are added
    854
    855
    0
        score.clamp(0.0, 1.0)
    856
    0
    }
    857
    858
    /// Analyze algorithmic complexity patterns
    859
    0
    fn analyze_algorithmic_complexity(ast: &crate::frontend::ast::Expr) -> f64 {
    860
    0
        let mut nested_loops = 0;
    861
    0
        let mut recursive_calls = 0;
    862
    863
    0
        analyze_complexity_recursive(ast, &mut nested_loops, &mut recursive_calls, 0);
    864
    865
    0
        let mut score = 1.0;
    866
    867
        // Penalize nested loops (O(n^k) complexity)
    868
    0
        if nested_loops > 0 {
    869
    0
            #[allow(clippy::cast_precision_loss)]
    870
    0
            let penalty = (f64::from(nested_loops) * 0.15).min(0.5);
    871
    0
            score *= 1.0 - penalty;
    872
    0
        }
    873
    874
        // Penalize recursive calls without obvious base case
    875
    0
        if recursive_calls > 2 {
    876
    0
            score *= 0.8; // May indicate exponential complexity
    877
    0
        }
    878
    879
    0
        score
    880
    0
    }
    881
    882
    0
    fn analyze_complexity_recursive(
    883
    0
        expr: &crate::frontend::ast::Expr,
    884
    0
        nested_loops: &mut i32,
    885
    0
        recursive_calls: &mut i32,
    886
    0
        current_nesting: i32,
    887
    0
    ) {
    888
    889
    0
        match &expr.kind {
    890
    0
            ExprKind::For { iter, body, .. } | ExprKind::While { condition: iter, body } => {
    891
    0
                if current_nesting > 0 {
    892
    0
                    *nested_loops += 1;
    893
    0
                }
    894
    0
                analyze_complexity_recursive(iter, nested_loops, recursive_calls, current_nesting);
    895
    0
                analyze_complexity_recursive(body, nested_loops, recursive_calls, current_nesting + 1);
    896
            }
    897
    0
            ExprKind::Call { func, args } => {
    898
                // Check for potential recursive calls (heuristic)
    899
    0
                if let ExprKind::Identifier(_) = &func.kind {
    900
    0
                    *recursive_calls += 1;
    901
    0
                }
    902
    0
                analyze_complexity_recursive(func, nested_loops, recursive_calls, current_nesting);
    903
    0
                for arg in args {
    904
    0
                    analyze_complexity_recursive(arg, nested_loops, recursive_calls, current_nesting);
    905
    0
                }
    906
            }
    907
    0
            ExprKind::Block(exprs) => {
    908
    0
                for e in exprs {
    909
    0
                    analyze_complexity_recursive(e, nested_loops, recursive_calls, current_nesting);
    910
    0
                }
    911
            }
    912
    0
            ExprKind::Function { body, .. } => {
    913
    0
                analyze_complexity_recursive(body, nested_loops, recursive_calls, 0);
    914
    0
            }
    915
    0
            ExprKind::If { condition, then_branch, else_branch } => {
    916
    0
                analyze_complexity_recursive(condition, nested_loops, recursive_calls, current_nesting);
    917
    0
                analyze_complexity_recursive(then_branch, nested_loops, recursive_calls, current_nesting);
    918
    0
                if let Some(else_expr) = else_branch {
    919
    0
                    analyze_complexity_recursive(else_expr, nested_loops, recursive_calls, current_nesting);
    920
    0
                }
    921
            }
    922
    0
            _ => {}
    923
        }
    924
    0
    }
    925
    926
    /// Analyze allocation patterns (GC pressure)
    927
    0
    fn analyze_allocation_patterns(ast: &crate::frontend::ast::Expr) -> f64 {
    928
    0
        let mut allocations = 0;
    929
    0
        let mut large_allocations = 0;
    930
    931
    0
        count_allocations_recursive(ast, &mut allocations, &mut large_allocations);
    932
    933
    0
        let mut score = 1.0;
    934
    935
        // Penalize excessive allocations
    936
    0
        if allocations > 10 {
    937
    0
            #[allow(clippy::cast_precision_loss)]
    938
    0
            let penalty = (f64::from(allocations - 10) * 0.01).min(0.3);
    939
    0
            score *= 1.0 - penalty;
    940
    0
        }
    941
    942
        // Penalize large allocations in loops
    943
    0
        if large_allocations > 0 {
    944
    0
            #[allow(clippy::cast_precision_loss)]
    945
    0
            let penalty = (f64::from(large_allocations) * 0.1).min(0.4);
    946
    0
            score *= 1.0 - penalty;
    947
    0
        }
    948
    949
    0
        score
    950
    0
    }
    951
    952
    0
    fn count_allocations_recursive(
    953
    0
        expr: &crate::frontend::ast::Expr,
    954
    0
        allocations: &mut i32,
    955
    0
        large_allocations: &mut i32,
    956
    0
    ) {
    957
    958
    0
        match &expr.kind {
    959
    0
            ExprKind::List(items) => {
    960
    0
                *allocations += 1;
    961
    0
                if items.len() > 100 {
    962
    0
                    *large_allocations += 1;
    963
    0
                }
    964
    0
                for item in items {
    965
    0
                    count_allocations_recursive(item, allocations, large_allocations);
    966
    0
                }
    967
            }
    968
            // Dictionary literals not yet implemented in AST
    969
            // ExprKind::Dict { pairs } => { ... }
    970
    0
            ExprKind::StringInterpolation { parts } => {
    971
    0
                *allocations += 1; // String concatenation
    972
    0
                for part in parts {
    973
    0
                    if let crate::frontend::ast::StringPart::Expr(e) = part {
    974
    0
                        count_allocations_recursive(e, allocations, large_allocations);
    975
    0
                    }
    976
                }
    977
            }
    978
    0
            ExprKind::Block(exprs) => {
    979
    0
                for e in exprs {
    980
    0
                    count_allocations_recursive(e, allocations, large_allocations);
    981
    0
                }
    982
            }
    983
    0
            ExprKind::Function { body, .. } => {
    984
    0
                count_allocations_recursive(body, allocations, large_allocations);
    985
    0
            }
    986
    0
            _ => {}
    987
        }
    988
    0
    }
    989
    990
    // Memory access pattern analysis will be added when
    991
    // AST supports indexing and dictionary operations
    992
    993
    /// Score maintainability component (20% weight)
    994
    0
    pub fn score_maintainability(ast: &crate::frontend::ast::Expr) -> f64 {
    995
    0
        let metrics = analyze_ast_metrics(ast);
    996
    0
        let mut score = 1.0;
    997
    998
        // Coupling analysis
    999
    0
        let coupling_score = analyze_coupling(ast);
    1000
    0
        score *= coupling_score;
    1001
    1002
        // Cohesion analysis
    1003
    0
        let cohesion_score = analyze_cohesion(ast, &metrics);
    1004
    0
        score *= cohesion_score;
    1005
    1006
        // Code duplication detection (simplified)
    1007
    0
        let duplication_score = analyze_duplication(ast);
    1008
    0
        score *= duplication_score;
    1009
    1010
        // Naming quality (basic heuristics)
    1011
    0
        let naming_score = analyze_naming_quality(ast);
    1012
    0
        score *= naming_score;
    1013
    1014
    0
        score.clamp(0.0, 1.0)
    1015
    0
    }
    1016
    1017
    /// Analyze coupling between functions/modules
    1018
    0
    fn analyze_coupling(ast: &crate::frontend::ast::Expr) -> f64 {
    1019
    0
        let mut external_calls = 0;
    1020
    0
        let mut total_functions = 0;
    1021
    1022
    0
        count_coupling_metrics(ast, &mut external_calls, &mut total_functions);
    1023
    1024
    0
        if total_functions == 0 {
    1025
    0
            return 1.0;
    1026
    0
        }
    1027
    1028
        #[allow(clippy::cast_precision_loss)]
    1029
    0
        let coupling_ratio = f64::from(external_calls) / f64::from(total_functions);
    1030
    1031
        // Lower coupling is better
    1032
    0
        if coupling_ratio > 5.0 {
    1033
    0
            0.7 // High coupling penalty
    1034
    0
        } else if coupling_ratio > 2.0 {
    1035
    0
            0.85
    1036
        } else {
    1037
    0
            1.0 // Good coupling
    1038
        }
    1039
    0
    }
    1040
    1041
    0
    fn count_coupling_metrics(expr: &crate::frontend::ast::Expr, external_calls: &mut i32, total_functions: &mut i32) {
    1042
    1043
    0
        match &expr.kind {
    1044
    0
            ExprKind::Function { body, .. } => {
    1045
    0
                *total_functions += 1;
    1046
    0
                count_coupling_metrics(body, external_calls, total_functions);
    1047
    0
            }
    1048
    0
            ExprKind::Call { func, args } => {
    1049
    0
                *external_calls += 1;
    1050
    0
                count_coupling_metrics(func, external_calls, total_functions);
    1051
    0
                for arg in args {
    1052
    0
                    count_coupling_metrics(arg, external_calls, total_functions);
    1053
    0
                }
    1054
            }
    1055
    0
            ExprKind::Block(exprs) => {
    1056
    0
                for e in exprs {
    1057
    0
                    count_coupling_metrics(e, external_calls, total_functions);
    1058
    0
                }
    1059
            }
    1060
    0
            _ => {}
    1061
        }
    1062
    0
    }
    1063
    1064
    /// Analyze cohesion within functions
    1065
    0
    fn analyze_cohesion(_ast: &crate::frontend::ast::Expr, metrics: &AstMetrics) -> f64 {
    1066
    0
        let mut score = 1.0;
    1067
    1068
        // Penalize functions that are too large (low cohesion indicator)
    1069
    0
        if metrics.line_count > 100 {
    1070
    0
            score *= 0.8;
    1071
    0
        }
    1072
    1073
        // Penalize excessive depth (indicates mixed concerns)
    1074
    0
        if metrics.max_depth > 15 {
    1075
    0
            score *= 0.85;
    1076
    0
        }
    1077
    1078
        // Penalize too many functions (possible low cohesion)
    1079
    0
        if metrics.function_count > 30 {
    1080
    0
            score *= 0.9;
    1081
    0
        }
    1082
    1083
    0
        score
    1084
    0
    }
    1085
    1086
    /// Analyze code duplication (simplified heuristic)
    1087
    0
    fn analyze_duplication(ast: &crate::frontend::ast::Expr) -> f64 {
    1088
    0
        let mut expression_patterns = std::collections::HashMap::new();
    1089
    1090
    0
        collect_expression_patterns(ast, &mut expression_patterns);
    1091
    1092
    0
        let duplicated_patterns = expression_patterns.values().filter(|&&count| count > 1).count();
    1093
    1094
    0
        if duplicated_patterns > 5 {
    1095
    0
            0.8 // Significant duplication penalty
    1096
    0
        } else if duplicated_patterns > 2 {
    1097
    0
            0.9 // Some duplication
    1098
        } else {
    1099
    0
            1.0 // Little to no duplication
    1100
        }
    1101
    0
    }
    1102
    1103
    0
    fn collect_expression_patterns(expr: &crate::frontend::ast::Expr, patterns: &mut std::collections::HashMap<String, i32>) {
    1104
    1105
        // Simplified pattern matching - use expression kind as pattern
    1106
    0
        let pattern = format!("{:?}", std::mem::discriminant(&expr.kind));
    1107
    0
        *patterns.entry(pattern).or_insert(0) += 1;
    1108
    1109
    0
        match &expr.kind {
    1110
    0
            ExprKind::Block(exprs) => {
    1111
    0
                for e in exprs {
    1112
    0
                    collect_expression_patterns(e, patterns);
    1113
    0
                }
    1114
            }
    1115
    0
            ExprKind::Function { body, .. } => {
    1116
    0
                collect_expression_patterns(body, patterns);
    1117
    0
            }
    1118
    0
            ExprKind::If { condition, then_branch, else_branch } => {
    1119
    0
                collect_expression_patterns(condition, patterns);
    1120
    0
                collect_expression_patterns(then_branch, patterns);
    1121
    0
                if let Some(else_expr) = else_branch {
    1122
    0
                    collect_expression_patterns(else_expr, patterns);
    1123
    0
                }
    1124
            }
    1125
    0
            _ => {}
    1126
        }
    1127
    0
    }
    1128
    1129
    /// Analyze naming quality (basic heuristics)
    1130
    0
    fn analyze_naming_quality(ast: &crate::frontend::ast::Expr) -> f64 {
    1131
    0
        let mut good_names = 0;
    1132
    0
        let mut total_names = 0;
    1133
    1134
    0
        analyze_names_recursive(ast, &mut good_names, &mut total_names);
    1135
    1136
    0
        if total_names == 0 {
    1137
    0
            return 1.0;
    1138
    0
        }
    1139
    1140
        #[allow(clippy::cast_precision_loss)]
    1141
    0
        let good_ratio = f64::from(good_names) / f64::from(total_names);
    1142
    0
        good_ratio.max(0.5) // Minimum score for naming
    1143
    0
    }
    1144
    1145
    0
    fn analyze_names_recursive(expr: &crate::frontend::ast::Expr, good_names: &mut i32, total_names: &mut i32) {
    1146
    1147
    0
        match &expr.kind {
    1148
    0
            ExprKind::Function { name, .. } => {
    1149
    0
                *total_names += 1;
    1150
    0
                if is_good_name(name) {
    1151
    0
                    *good_names += 1;
    1152
    0
                }
    1153
            }
    1154
    0
            ExprKind::Let { name, body, .. } => {
    1155
    0
                *total_names += 1;
    1156
    0
                if is_good_name(name) {
    1157
    0
                    *good_names += 1;
    1158
    0
                }
    1159
    0
                analyze_names_recursive(body, good_names, total_names);
    1160
            }
    1161
    0
            ExprKind::Block(exprs) => {
    1162
    0
                for e in exprs {
    1163
    0
                    analyze_names_recursive(e, good_names, total_names);
    1164
    0
                }
    1165
            }
    1166
    0
            _ => {}
    1167
        }
    1168
    0
    }
    1169
    1170
    0
    fn is_good_name(name: &str) -> bool {
    1171
        // Basic heuristics for good naming
    1172
    0
        if name.len() < 2 || name.starts_with('_') {
    1173
    0
            return false;
    1174
    0
        }
    1175
    1176
        // Check for descriptive names (not single letters or abbreviations)
    1177
    0
        name.len() >= 3 && !name.chars().all(|c| c.is_ascii_uppercase())
    1178
    0
    }
    1179
    1180
    /// Score safety component (15% weight)
    1181
    0
    pub fn score_safety(ast: &crate::frontend::ast::Expr) -> f64 {
    1182
    0
        let mut score = 1.0;
    1183
    1184
        // Error handling coverage (reuse from correctness)
    1185
    0
        let error_handling_quality = analyze_error_handling(ast);
    1186
    0
        score *= error_handling_quality;
    1187
    1188
        // Null safety analysis
    1189
    0
        let null_safety_score = analyze_null_safety(ast);
    1190
    0
        score *= null_safety_score;
    1191
    1192
        // Resource management analysis
    1193
    0
        let resource_score = analyze_resource_management(ast);
    1194
    0
        score *= resource_score;
    1195
    1196
        // Bounds checking (implicit in type system, give good score)
    1197
    0
        score *= 0.95; // Slight penalty for not having explicit bounds checks
    1198
    1199
    0
        score.clamp(0.0, 1.0)
    1200
    0
    }
    1201
    1202
    /// Analyze null safety patterns
    1203
    0
    fn analyze_null_safety(ast: &crate::frontend::ast::Expr) -> f64 {
    1204
    0
        let mut option_uses = 0;
    1205
    0
        let mut unsafe_accesses = 0;
    1206
    1207
    0
        analyze_null_safety_recursive(ast, &mut option_uses, &mut unsafe_accesses);
    1208
    1209
    0
        if option_uses + unsafe_accesses == 0 {
    1210
    0
            return 1.0; // No nullable types used
    1211
    0
        }
    1212
    1213
        // Prefer Option types over unsafe accesses
    1214
    0
        if unsafe_accesses == 0 {
    1215
    0
            1.0 // All nullable accesses are safe
    1216
        } else {
    1217
            #[allow(clippy::cast_precision_loss)]
    1218
    0
            let safety_ratio = f64::from(option_uses) / f64::from(option_uses + unsafe_accesses);
    1219
    0
            safety_ratio.max(0.5) // Minimum safety score
    1220
        }
    1221
    0
    }
    1222
    1223
    0
    fn analyze_null_safety_recursive(expr: &crate::frontend::ast::Expr, option_uses: &mut i32, unsafe_accesses: &mut i32) {
    1224
    1225
    0
        match &expr.kind {
    1226
    0
            ExprKind::Some { .. } | ExprKind::None => {
    1227
    0
                *option_uses += 1;
    1228
    0
            }
    1229
    0
            ExprKind::Match { arms, .. } => {
    1230
                // Check if matching on Option (heuristic)
    1231
    0
                if arms.len() >= 2 {
    1232
    0
                    *option_uses += 1;
    1233
    0
                }
    1234
    0
                for arm in arms {
    1235
    0
                    analyze_null_safety_recursive(&arm.body, option_uses, unsafe_accesses);
    1236
    0
                }
    1237
            }
    1238
    0
            ExprKind::Block(exprs) => {
    1239
    0
                for e in exprs {
    1240
    0
                    analyze_null_safety_recursive(e, option_uses, unsafe_accesses);
    1241
    0
                }
    1242
            }
    1243
    0
            ExprKind::Function { body, .. } => {
    1244
    0
                analyze_null_safety_recursive(body, option_uses, unsafe_accesses);
    1245
    0
            }
    1246
    0
            _ => {}
    1247
        }
    1248
    0
    }
    1249
    1250
    /// Analyze resource management patterns
    1251
    0
    fn analyze_resource_management(ast: &crate::frontend::ast::Expr) -> f64 {
    1252
    0
        let mut resource_allocations = 0;
    1253
    0
        let mut proper_cleanup = 0;
    1254
    1255
    0
        analyze_resources_recursive(ast, &mut resource_allocations, &mut proper_cleanup);
    1256
    1257
    0
        if resource_allocations == 0 {
    1258
    0
            return 1.0; // No resources to manage
    1259
    0
        }
    1260
    1261
        #[allow(clippy::cast_precision_loss)]
    1262
    0
        let cleanup_ratio = f64::from(proper_cleanup) / f64::from(resource_allocations);
    1263
    0
        cleanup_ratio.max(0.7) // Minimum score for resource management
    1264
    0
    }
    1265
    1266
    0
    fn analyze_resources_recursive(expr: &crate::frontend::ast::Expr, allocations: &mut i32, cleanup: &mut i32) {
    1267
    1268
    0
        match &expr.kind {
    1269
    0
            ExprKind::Block(exprs) => {
    1270
    0
                for e in exprs {
    1271
    0
                    analyze_resources_recursive(e, allocations, cleanup);
    1272
    0
                }
    1273
            }
    1274
    0
            ExprKind::Function { body, .. } => {
    1275
    0
                analyze_resources_recursive(body, allocations, cleanup);
    1276
    0
            }
    1277
    0
            _ => {}
    1278
        }
    1279
    0
    }
    1280
    1281
    /// Score idiomaticity component (5% weight)
    1282
    0
    pub fn score_idiomaticity(ast: &crate::frontend::ast::Expr) -> f64 {
    1283
    0
        let mut score = 1.0;
    1284
    1285
        // Pattern matching usage (idiomatic in Ruchy)
    1286
    0
        let pattern_score = analyze_pattern_usage(ast);
    1287
    0
        score *= pattern_score;
    1288
    1289
        // Iterator usage (functional style)
    1290
    0
        let iterator_score = analyze_iterator_usage(ast);
    1291
    0
        score *= iterator_score;
    1292
    1293
        // Lambda usage (functional programming)
    1294
    0
        let lambda_score = analyze_lambda_usage(ast);
    1295
    0
        score *= lambda_score;
    1296
    1297
    0
        score.clamp(0.0, 1.0)
    1298
    0
    }
    1299
    1300
    /// Analyze usage of pattern matching (idiomatic)
    1301
    0
    fn analyze_pattern_usage(ast: &crate::frontend::ast::Expr) -> f64 {
    1302
    0
        let mut matches = 0;
    1303
    0
        let mut conditionals = 0;
    1304
    1305
    0
        count_pattern_vs_conditional(ast, &mut matches, &mut conditionals);
    1306
    1307
    0
        let total = matches + conditionals;
    1308
    0
        if total == 0 {
    1309
    0
            return 1.0;
    1310
    0
        }
    1311
    1312
        #[allow(clippy::cast_precision_loss)]
    1313
    0
        let pattern_ratio = f64::from(matches) / f64::from(total);
    1314
    1315
        // Higher ratio of matches vs if-else is more idiomatic
    1316
    0
        if pattern_ratio > 0.7 {
    1317
    0
            1.0
    1318
    0
        } else if pattern_ratio > 0.4 {
    1319
    0
            0.9
    1320
        } else {
    1321
    0
            0.8
    1322
        }
    1323
    0
    }
    1324
    1325
    0
    fn count_pattern_vs_conditional(expr: &crate::frontend::ast::Expr, matches: &mut i32, conditionals: &mut i32) {
    1326
    1327
    0
        match &expr.kind {
    1328
    0
            ExprKind::Match { arms, .. } => {
    1329
    0
                *matches += 1;
    1330
    0
                for arm in arms {
    1331
    0
                    count_pattern_vs_conditional(&arm.body, matches, conditionals);
    1332
    0
                }
    1333
            }
    1334
    0
            ExprKind::If { condition, then_branch, else_branch } => {
    1335
    0
                *conditionals += 1;
    1336
    0
                count_pattern_vs_conditional(condition, matches, conditionals);
    1337
    0
                count_pattern_vs_conditional(then_branch, matches, conditionals);
    1338
    0
                if let Some(else_expr) = else_branch {
    1339
    0
                    count_pattern_vs_conditional(else_expr, matches, conditionals);
    1340
    0
                }
    1341
            }
    1342
    0
            ExprKind::Block(exprs) => {
    1343
    0
                for e in exprs {
    1344
    0
                    count_pattern_vs_conditional(e, matches, conditionals);
    1345
    0
                }
    1346
            }
    1347
    0
            ExprKind::Function { body, .. } => {
    1348
    0
                count_pattern_vs_conditional(body, matches, conditionals);
    1349
    0
            }
    1350
    0
            _ => {}
    1351
        }
    1352
    0
    }
    1353
    1354
    /// Analyze iterator usage patterns
    1355
    0
    fn analyze_iterator_usage(ast: &crate::frontend::ast::Expr) -> f64 {
    1356
    0
        let mut iterators = 0;
    1357
    0
        let mut loops = 0;
    1358
    1359
    0
        count_iterator_vs_loops(ast, &mut iterators, &mut loops);
    1360
    1361
    0
        let total = iterators + loops;
    1362
    0
        if total == 0 {
    1363
    0
            return 1.0;
    1364
    0
        }
    1365
    1366
        #[allow(clippy::cast_precision_loss)]
    1367
    0
        let iterator_ratio = f64::from(iterators) / f64::from(total);
    1368
    1369
        // Higher ratio of iterators vs manual loops is more idiomatic
    1370
    0
        if iterator_ratio > 0.6 {
    1371
    0
            1.0
    1372
    0
        } else if iterator_ratio > 0.3 {
    1373
    0
            0.9
    1374
        } else {
    1375
    0
            0.8
    1376
        }
    1377
    0
    }
    1378
    1379
    0
    fn count_iterator_vs_loops(expr: &crate::frontend::ast::Expr, iterators: &mut i32, loops: &mut i32) {
    1380
    1381
    0
        match &expr.kind {
    1382
    0
            ExprKind::For { .. } => {
    1383
    0
                *iterators += 1; // For loops in Ruchy are iterator-based
    1384
    0
            }
    1385
    0
            ExprKind::While { .. } => {
    1386
    0
                *loops += 1;
    1387
    0
            }
    1388
    0
            ExprKind::Block(exprs) => {
    1389
    0
                for e in exprs {
    1390
    0
                    count_iterator_vs_loops(e, iterators, loops);
    1391
    0
                }
    1392
            }
    1393
    0
            ExprKind::Function { body, .. } => {
    1394
    0
                count_iterator_vs_loops(body, iterators, loops);
    1395
    0
            }
    1396
    0
            _ => {}
    1397
        }
    1398
    0
    }
    1399
    1400
    /// Analyze lambda/closure usage
    1401
    0
    fn analyze_lambda_usage(ast: &crate::frontend::ast::Expr) -> f64 {
    1402
    0
        let mut lambdas = 0;
    1403
    0
        let mut total_functions = 0;
    1404
    1405
    0
        count_lambda_usage(ast, &mut lambdas, &mut total_functions);
    1406
    1407
    0
        if total_functions == 0 {
    1408
    0
            return 1.0;
    1409
    0
        }
    1410
    1411
        #[allow(clippy::cast_precision_loss)]
    1412
    0
        let lambda_ratio = f64::from(lambdas) / f64::from(total_functions);
    1413
    1414
        // Some lambda usage indicates functional style
    1415
    0
        if lambda_ratio > 0.3 {
    1416
    0
            1.0
    1417
    0
        } else if lambda_ratio > 0.1 {
    1418
    0
            0.95
    1419
        } else {
    1420
    0
            0.9 // Still good if other patterns are used
    1421
        }
    1422
    0
    }
    1423
    1424
    0
    fn count_lambda_usage(expr: &crate::frontend::ast::Expr, lambdas: &mut i32, total_functions: &mut i32) {
    1425
    1426
    0
        match &expr.kind {
    1427
    0
            ExprKind::Lambda { .. } => {
    1428
    0
                *lambdas += 1;
    1429
    0
                *total_functions += 1;
    1430
    0
            }
    1431
    0
            ExprKind::Function { body, .. } => {
    1432
    0
                *total_functions += 1;
    1433
    0
                count_lambda_usage(body, lambdas, total_functions);
    1434
    0
            }
    1435
    0
            ExprKind::Block(exprs) => {
    1436
    0
                for e in exprs {
    1437
    0
                    count_lambda_usage(e, lambdas, total_functions);
    1438
    0
                }
    1439
            }
    1440
    0
            _ => {}
    1441
        }
    1442
    0
    }