Skip to main content

tldr_core/dataflow/
abstract_interp.rs

1//! Abstract Interpretation Analysis
2//!
3//! This module implements forward dataflow analysis with abstract interpretation
4//! for tracking variable ranges, nullability, and detecting potential issues.
5//!
6//! ## Capabilities Implemented (Phase 5)
7//!
8//! - CAP-AI-01: Nullability enum (Never, Maybe, Always)
9//! - CAP-AI-02: AbstractValue struct with type_, range_, nullable, constant
10//! - CAP-AI-03: ConstantValue enum for tracked constants
11//! - CAP-AI-04: top() and bottom() lattice elements
12//! - CAP-AI-05: may_be_zero() for division-by-zero checks
13//! - CAP-AI-06: may_be_null() for null dereference checks
14//! - CAP-AI-07: AbstractState mapping variables to abstract values
15//! - CAP-AI-21: AbstractInterpInfo result struct
16//! - CAP-AI-22: to_json() serialization
17//!
18//! ## TIGER Mitigations
19//!
20//! - TIGER-PASS1-11: Use saturating arithmetic for range operations
21//! - TIGER-PASS2-5: Document JSON infinity representation (null = unbounded)
22//! - TIGER-PASS2-8: Track TypeScript undefined vs null as different types
23
24use std::collections::HashMap;
25use std::hash::{Hash, Hasher};
26
27use serde::{Deserialize, Serialize};
28use serde_json;
29
30use super::types::BlockId;
31
32// =============================================================================
33// CAP-AI-01: Nullability Enum
34// =============================================================================
35
36/// Nullability lattice: NEVER < MAYBE < ALWAYS
37///
38/// Used to track whether a variable may be null/None at a program point.
39///
40/// # Lattice Order
41///
42/// ```text
43///        MAYBE (top - unknown)
44///       /     \
45///   NEVER    ALWAYS
46///       \     /
47///        (bottom - contradiction, not representable)
48/// ```
49///
50/// # Default
51///
52/// Defaults to `Maybe` (unknown nullability) per spec CAP-AI-01.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54#[serde(rename_all = "lowercase")]
55pub enum Nullability {
56    /// Definitely not null - safe to dereference
57    Never,
58    /// Could be null or non-null - requires null check
59    #[serde(rename = "maybe")]
60    Maybe,
61    /// Definitely null - will fail on dereference
62    Always,
63}
64
65impl Default for Nullability {
66    /// CAP-AI-01: Default is Maybe (unknown)
67    fn default() -> Self {
68        Nullability::Maybe
69    }
70}
71
72impl Nullability {
73    /// Convert to string representation for JSON output
74    pub fn as_str(&self) -> &'static str {
75        match self {
76            Nullability::Never => "never",
77            Nullability::Maybe => "maybe",
78            Nullability::Always => "always",
79        }
80    }
81}
82
83// =============================================================================
84// CAP-AI-03: ConstantValue Enum
85// =============================================================================
86
87/// Constant values that can be tracked during abstract interpretation.
88///
89/// Supports integers, floats, strings, booleans, and null values.
90///
91/// # JSON Representation
92///
93/// Values serialize directly to their JSON equivalents:
94/// - Int(5) -> 5
95/// - Float(3.14) -> 3.14
96/// - String("hello") -> "hello"
97/// - Bool(true) -> true
98/// - Null -> null
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(untagged)]
101pub enum ConstantValue {
102    /// Integer constant (i64 range)
103    Int(i64),
104    /// Floating-point constant
105    Float(f64),
106    /// String constant
107    String(String),
108    /// Boolean constant
109    Bool(bool),
110    /// Null/None/nil constant
111    Null,
112}
113
114// Manual PartialEq to handle float comparison
115impl PartialEq for ConstantValue {
116    fn eq(&self, other: &Self) -> bool {
117        match (self, other) {
118            (ConstantValue::Int(a), ConstantValue::Int(b)) => a == b,
119            (ConstantValue::Float(a), ConstantValue::Float(b)) => {
120                // Handle NaN and exact equality
121                (a.is_nan() && b.is_nan()) || a == b
122            }
123            (ConstantValue::String(a), ConstantValue::String(b)) => a == b,
124            (ConstantValue::Bool(a), ConstantValue::Bool(b)) => a == b,
125            (ConstantValue::Null, ConstantValue::Null) => true,
126            _ => false,
127        }
128    }
129}
130
131impl ConstantValue {
132    /// Convert to JSON value for serialization
133    pub fn to_json_value(&self) -> serde_json::Value {
134        match self {
135            ConstantValue::Int(v) => serde_json::json!(v),
136            ConstantValue::Float(v) => serde_json::json!(v),
137            ConstantValue::String(v) => serde_json::json!(v),
138            ConstantValue::Bool(v) => serde_json::json!(v),
139            ConstantValue::Null => serde_json::Value::Null,
140        }
141    }
142}
143
144// =============================================================================
145// CAP-AI-02 to CAP-AI-06: AbstractValue
146// =============================================================================
147
148/// Abstract representation of a variable's value at a program point.
149///
150/// Tracks four dimensions:
151/// - `type_`: Inferred type (str, int, list, etc.) or None if unknown
152/// - `range_`: Value range [min, max] for numeric types, None for unbounded
153/// - `nullable`: Whether the value can be null/None
154/// - `constant`: If value is a known constant, the value itself
155///
156/// # Range Representation
157///
158/// The `range_` field uses `Option<(Option<i64>, Option<i64>)>`:
159/// - `None` outer: No range information (unknown)
160/// - `Some((None, None))`: Unbounded range (-inf, +inf)
161/// - `Some((Some(5), Some(5)))`: Exact value [5, 5]
162/// - `Some((Some(1), None))`: Lower bound only [1, +inf)
163/// - `Some((None, Some(10)))`: Upper bound only (-inf, 10]
164///
165/// # JSON Infinity Representation (TIGER-PASS2-5)
166///
167/// In JSON output:
168/// - `null` in range array position = infinity (unbounded)
169/// - Example: `"range": [null, 10]` means (-inf, 10]
170///
171/// # TIGER-PASS1-11: Saturating Arithmetic
172///
173/// All arithmetic operations on ranges use saturating operations to prevent overflow.
174/// When overflow would occur, the bound is widened to infinity (None).
175#[derive(Debug, Clone)]
176pub struct AbstractValue {
177    /// Inferred type name (e.g., "int", "str") or None if unknown
178    pub type_: Option<String>,
179
180    /// Value range [min, max] for numerics. None bounds mean infinity.
181    /// For strings, tracks length.
182    pub range_: Option<(Option<i64>, Option<i64>)>,
183
184    /// Nullability status
185    pub nullable: Nullability,
186
187    /// Known constant value (used for constant propagation)
188    pub constant: Option<ConstantValue>,
189}
190
191// Manual PartialEq to handle constant comparison properly
192impl PartialEq for AbstractValue {
193    fn eq(&self, other: &Self) -> bool {
194        self.type_ == other.type_
195            && self.range_ == other.range_
196            && self.nullable == other.nullable
197            && self.constant == other.constant
198    }
199}
200
201// Note: Eq is implemented even though ConstantValue contains f64
202// because we handle NaN comparison in ConstantValue::eq
203impl Eq for AbstractValue {}
204
205impl Hash for AbstractValue {
206    fn hash<H: Hasher>(&self, state: &mut H) {
207        // CAP-AI-02: AbstractValue must be hashable for use in sets
208        self.type_.hash(state);
209        self.range_.hash(state);
210        self.nullable.hash(state);
211        // Note: constant is NOT hashed per spec - equality by structure only
212    }
213}
214
215impl AbstractValue {
216    /// CAP-AI-04: Top of lattice - no information known (most permissive)
217    ///
218    /// Returns an abstract value representing complete uncertainty:
219    /// - Unknown type
220    /// - Unknown range
221    /// - Maybe nullable
222    /// - No constant value
223    ///
224    /// This is the default for variables with no information.
225    pub fn top() -> Self {
226        AbstractValue {
227            type_: None,
228            range_: None,
229            nullable: Nullability::Maybe,
230            constant: None,
231        }
232    }
233
234    /// CAP-AI-04: Bottom of lattice - contradiction (unreachable code)
235    ///
236    /// Returns an abstract value representing impossibility.
237    /// Used for unreachable code paths.
238    ///
239    /// Represented as:
240    /// - Type = "<bottom>"
241    /// - Range = (None, None) - representing contradiction
242    /// - Nullable = Never (contradicts Always)
243    /// - No constant
244    pub fn bottom() -> Self {
245        AbstractValue {
246            type_: Some("<bottom>".to_string()),
247            range_: Some((None, None)),
248            nullable: Nullability::Never,
249            constant: None,
250        }
251    }
252
253    /// CAP-AI-03: Create from known constant value
254    ///
255    /// Creates an abstract value with precise information from a constant:
256    ///
257    /// | Constant Type | type_ | range_ | nullable | constant |
258    /// |---------------|-------|--------|----------|----------|
259    /// | Int(v) | "int" | [v, v] | Never | Some(Int(v)) |
260    /// | Float(v) | "float" | None | Never | Some(Float(v)) |
261    /// | String(s) | "str" | [len, len] | Never | Some(String(s)) |
262    /// | Bool(v) | "bool" | [v as i64, v as i64] | Never | Some(Bool(v)) |
263    /// | Null | "NoneType" | None | Always | None |
264    ///
265    /// # TIGER-PASS2-8: TypeScript undefined vs null
266    ///
267    /// TypeScript `undefined` is tracked separately from `null`:
268    /// - `null` -> type_ = "null"
269    /// - `undefined` -> type_ = "undefined"
270    ///
271    /// Both have nullable = Always.
272    pub fn from_constant(value: ConstantValue) -> Self {
273        match value {
274            ConstantValue::Int(v) => AbstractValue {
275                type_: Some("int".to_string()),
276                range_: Some((Some(v), Some(v))),
277                nullable: Nullability::Never,
278                constant: Some(ConstantValue::Int(v)),
279            },
280            ConstantValue::Float(v) => AbstractValue {
281                type_: Some("float".to_string()),
282                range_: None, // Float ranges less useful
283                nullable: Nullability::Never,
284                constant: Some(ConstantValue::Float(v)),
285            },
286            ConstantValue::String(ref s) => {
287                let len = s.len() as i64;
288                AbstractValue {
289                    type_: Some("str".to_string()),
290                    // CAP-AI-18: Track string length in range
291                    range_: Some((Some(len), Some(len))),
292                    nullable: Nullability::Never,
293                    constant: Some(value),
294                }
295            }
296            ConstantValue::Bool(v) => AbstractValue {
297                type_: Some("bool".to_string()),
298                range_: Some((Some(v as i64), Some(v as i64))),
299                nullable: Nullability::Never,
300                constant: Some(ConstantValue::Bool(v)),
301            },
302            ConstantValue::Null => AbstractValue {
303                type_: Some("NoneType".to_string()),
304                range_: None,
305                nullable: Nullability::Always,
306                constant: None, // Null constant is represented by nullable=Always
307            },
308        }
309    }
310
311    /// CAP-AI-05: Check if value could be zero (for division check)
312    ///
313    /// Returns true if the range includes zero, indicating potential
314    /// division-by-zero if used as a divisor.
315    ///
316    /// # Logic
317    ///
318    /// - Unknown range (None) -> true (conservative)
319    /// - Range [low, high] where low <= 0 <= high -> true
320    /// - Range [1, 10] -> false (excludes zero)
321    /// - Range [-10, -1] -> false (excludes zero)
322    pub fn may_be_zero(&self) -> bool {
323        match &self.range_ {
324            None => true, // Unknown range, conservatively true
325            Some((low, high)) => {
326                let low = low.unwrap_or(i64::MIN);
327                let high = high.unwrap_or(i64::MAX);
328                low <= 0 && 0 <= high
329            }
330        }
331    }
332
333    /// CAP-AI-06: Check if value could be null/None
334    ///
335    /// Returns true if the value might be null, indicating potential
336    /// null dereference if used for attribute access.
337    ///
338    /// # Logic
339    ///
340    /// - Never -> false (safe to dereference)
341    /// - Maybe -> true (might be null)
342    /// - Always -> true (definitely null)
343    pub fn may_be_null(&self) -> bool {
344        self.nullable != Nullability::Never
345    }
346
347    /// Check if this is a known constant value
348    ///
349    /// Returns true if the constant field is set.
350    pub fn is_constant(&self) -> bool {
351        self.constant.is_some()
352    }
353
354    /// Convert to JSON-serializable format
355    ///
356    /// # JSON Format
357    ///
358    /// ```json
359    /// {
360    ///   "type": "int",           // or null if unknown
361    ///   "range": [5, 5],         // [low, high], null = infinity
362    ///   "nullable": "never",     // "never" | "maybe" | "always"
363    ///   "constant": 5            // only if known constant
364    /// }
365    /// ```
366    pub fn to_json_value(&self) -> serde_json::Value {
367        let mut obj = serde_json::Map::new();
368
369        if let Some(ref t) = self.type_ {
370            obj.insert("type".to_string(), serde_json::json!(t));
371        }
372
373        if let Some((low, high)) = &self.range_ {
374            // TIGER-PASS2-5: null in array = infinity
375            let range = serde_json::json!([low, high]);
376            obj.insert("range".to_string(), range);
377        }
378
379        obj.insert(
380            "nullable".to_string(),
381            serde_json::json!(self.nullable.as_str()),
382        );
383
384        if let Some(ref c) = self.constant {
385            obj.insert("constant".to_string(), c.to_json_value());
386        }
387
388        serde_json::Value::Object(obj)
389    }
390}
391
392// =============================================================================
393// CAP-AI-07: AbstractState
394// =============================================================================
395
396/// Abstract state at a program point: mapping from variables to abstract values.
397///
398/// Represents the known information about all variables at a specific point
399/// in the program. This is the dataflow fact for abstract interpretation.
400///
401/// # Immutable Update Pattern
402///
403/// AbstractState uses an immutable update pattern where `set()` returns
404/// a new state rather than mutating in place. This makes dataflow analysis
405/// easier to reason about.
406///
407/// # Default Values
408///
409/// Variables not in the map are treated as `top()` (unknown).
410#[derive(Debug, Clone, Default, PartialEq, Eq)]
411pub struct AbstractState {
412    /// Mapping from variable names to their abstract values
413    pub values: HashMap<String, AbstractValue>,
414}
415
416impl AbstractState {
417    /// Create a new empty state
418    pub fn new() -> Self {
419        Self::default()
420    }
421
422    /// Get abstract value for variable, defaulting to top (unknown)
423    ///
424    /// # Returns
425    ///
426    /// The abstract value for the variable if known, otherwise `top()`.
427    pub fn get(&self, var: &str) -> AbstractValue {
428        self.values
429            .get(var)
430            .cloned()
431            .unwrap_or_else(AbstractValue::top)
432    }
433
434    /// Return new state with updated variable value (immutable style)
435    ///
436    /// Creates a new AbstractState with the variable set to the given value.
437    /// The original state is unchanged.
438    ///
439    /// # Example
440    ///
441    /// ```rust,ignore
442    /// let state1 = AbstractState::new();
443    /// let state2 = state1.set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
444    /// // state1 is unchanged
445    /// // state2 has x = 5
446    /// ```
447    pub fn set(&self, var: &str, value: AbstractValue) -> Self {
448        let mut new_values = self.values.clone();
449        new_values.insert(var.to_string(), value);
450        AbstractState { values: new_values }
451    }
452
453    /// Create a copy of this state
454    ///
455    /// Equivalent to clone() but with explicit semantics.
456    pub fn copy(&self) -> Self {
457        self.clone()
458    }
459}
460
461// =============================================================================
462// CAP-AI-21 & CAP-AI-22: AbstractInterpInfo
463// =============================================================================
464
465/// Abstract interpretation analysis results for a function.
466///
467/// Contains the dataflow information at each block entry/exit,
468/// plus detected potential issues (div-by-zero, null deref).
469///
470/// # Query Methods
471///
472/// The struct provides convenient query methods:
473/// - `value_at(block, var)` - Get value at block entry
474/// - `value_at_exit(block, var)` - Get value at block exit
475/// - `range_at(block, var)` - Get range at block entry
476/// - `type_at(block, var)` - Get type at block entry
477/// - `is_definitely_not_null(block, var)` - Check if non-null at block entry
478/// - `get_constants()` - Get all constant values at function exit
479///
480/// # JSON Output (CAP-AI-22)
481///
482/// ```json
483/// {
484///   "function": "example",
485///   "state_in": { "0": { "x": {...} }, ... },
486///   "state_out": { "0": { "x": {...} }, ... },
487///   "potential_div_zero": [{"line": 10, "var": "y"}],
488///   "potential_null_deref": [{"line": 15, "var": "obj"}]
489/// }
490/// ```
491#[derive(Debug, Clone, Default)]
492pub struct AbstractInterpInfo {
493    /// Abstract state at entry of each block
494    pub state_in: HashMap<BlockId, AbstractState>,
495
496    /// Abstract state at exit of each block
497    pub state_out: HashMap<BlockId, AbstractState>,
498
499    /// CAP-AI-10: Potential division-by-zero warnings (line, var)
500    pub potential_div_zero: Vec<(usize, String)>,
501
502    /// CAP-AI-11: Potential null dereference warnings (line, var)
503    pub potential_null_deref: Vec<(usize, String)>,
504
505    /// Function name
506    pub function_name: String,
507}
508
509impl AbstractInterpInfo {
510    /// Create a new empty result for a function
511    pub fn new(function_name: &str) -> Self {
512        Self {
513            function_name: function_name.to_string(),
514            ..Default::default()
515        }
516    }
517
518    /// Get abstract value of variable at entry to block
519    ///
520    /// Returns top() if the block is not found or variable is not tracked.
521    pub fn value_at(&self, block: BlockId, var: &str) -> AbstractValue {
522        self.state_in
523            .get(&block)
524            .map(|s| s.get(var))
525            .unwrap_or_else(AbstractValue::top)
526    }
527
528    /// Get abstract value of variable at exit of block
529    ///
530    /// Returns top() if the block is not found or variable is not tracked.
531    pub fn value_at_exit(&self, block: BlockId, var: &str) -> AbstractValue {
532        self.state_out
533            .get(&block)
534            .map(|s| s.get(var))
535            .unwrap_or_else(AbstractValue::top)
536    }
537
538    /// Get the value range for variable at block entry
539    ///
540    /// Returns None if the variable has no range information.
541    pub fn range_at(&self, block: BlockId, var: &str) -> Option<(Option<i64>, Option<i64>)> {
542        self.value_at(block, var).range_
543    }
544
545    /// Get the inferred type for variable at block entry
546    ///
547    /// Returns None if the type is unknown.
548    pub fn type_at(&self, block: BlockId, var: &str) -> Option<String> {
549        self.value_at(block, var).type_
550    }
551
552    /// Check if variable is definitely non-null at block entry
553    ///
554    /// Returns true only if nullable == Never.
555    pub fn is_definitely_not_null(&self, block: BlockId, var: &str) -> bool {
556        self.value_at(block, var).nullable == Nullability::Never
557    }
558
559    /// CAP-AI-12: Get all variables with known constant values at function exit
560    ///
561    /// Scans all state_out blocks and collects variables with constant values.
562    pub fn get_constants(&self) -> HashMap<String, ConstantValue> {
563        let mut constants = HashMap::new();
564        for state in self.state_out.values() {
565            for (var, val) in &state.values {
566                if let Some(c) = &val.constant {
567                    constants.insert(var.clone(), c.clone());
568                }
569            }
570        }
571        constants
572    }
573
574    /// CAP-AI-22: Serialize to JSON-compatible structure
575    ///
576    /// Output format matches v1 CLI for compatibility.
577    pub fn to_json(&self) -> serde_json::Value {
578        let state_in: HashMap<String, serde_json::Value> = self
579            .state_in
580            .iter()
581            .map(|(k, state)| {
582                let vars: HashMap<String, serde_json::Value> = state
583                    .values
584                    .iter()
585                    .map(|(var, val)| (var.clone(), val.to_json_value()))
586                    .collect();
587                (k.to_string(), serde_json::json!(vars))
588            })
589            .collect();
590
591        let state_out: HashMap<String, serde_json::Value> = self
592            .state_out
593            .iter()
594            .map(|(k, state)| {
595                let vars: HashMap<String, serde_json::Value> = state
596                    .values
597                    .iter()
598                    .map(|(var, val)| (var.clone(), val.to_json_value()))
599                    .collect();
600                (k.to_string(), serde_json::json!(vars))
601            })
602            .collect();
603
604        let div_zero: Vec<_> = self
605            .potential_div_zero
606            .iter()
607            .map(|(line, var)| serde_json::json!({"line": line, "var": var}))
608            .collect();
609
610        let null_deref: Vec<_> = self
611            .potential_null_deref
612            .iter()
613            .map(|(line, var)| serde_json::json!({"line": line, "var": var}))
614            .collect();
615
616        serde_json::json!({
617            "function": self.function_name,
618            "state_in": state_in,
619            "state_out": state_out,
620            "potential_div_zero": div_zero,
621            "potential_null_deref": null_deref,
622        })
623    }
624}
625
626// =============================================================================
627// CAP-AI-15, CAP-AI-16, CAP-AI-17: Multi-Language Support (Phase 8)
628// =============================================================================
629
630/// CAP-AI-15: Get null-like keywords for a language.
631///
632/// Returns keywords that represent null/nil/None values in the given language.
633///
634/// # Language Support Table
635///
636/// | Language | Null Keywords |
637/// |----------|--------------|
638/// | Python | `["None"]` |
639/// | TypeScript/JavaScript | `["null", "undefined"]` |
640/// | Go | `["nil"]` |
641/// | Rust | `[]` (no null keyword, uses `Option`) |
642/// | Java/Kotlin/C# | `["null"]` |
643/// | Swift | `["nil"]` |
644/// | Unknown | `["null", "nil", "None"]` (fallback) |
645///
646/// # TIGER-PASS1-13 Mitigation
647///
648/// Covers all Language enum values with a sensible fallback for unknown languages.
649///
650/// # TIGER-PASS2-9 Mitigation (Go)
651///
652/// Note: Go nil detection is limited without type information. Go uses zero
653/// values for uninitialized variables (e.g., 0 for int, "" for string), which
654/// are distinct from nil. This function only detects explicit `nil` keywords.
655///
656/// # Examples
657///
658/// ```rust,ignore
659/// let keywords = get_null_keywords("python");
660/// assert!(keywords.contains(&"None"));
661///
662/// let keywords = get_null_keywords("rust");
663/// assert!(keywords.is_empty()); // Rust has no null keyword
664/// ```
665pub fn get_null_keywords(language: &str) -> Vec<&'static str> {
666    match language.to_lowercase().as_str() {
667        "python" => vec!["None"],
668        "typescript" | "javascript" => vec!["null", "undefined"],
669        "go" => vec!["nil"],
670        "rust" => vec![], // Rust has no null (None is Option::None, not a keyword)
671        "java" | "kotlin" | "csharp" | "c#" => vec!["null"],
672        "swift" => vec!["nil"],
673        _ => vec!["null", "nil", "None"], // Fallback for unknown languages
674    }
675}
676
677/// CAP-AI-16: Get boolean keywords for a language.
678///
679/// Returns a mapping from boolean keyword strings to their boolean values.
680///
681/// # Language Support Table
682///
683/// | Language | True Keyword | False Keyword |
684/// |----------|-------------|---------------|
685/// | Python | `True` | `False` |
686/// | TypeScript/JavaScript/Go/Rust | `true` | `false` |
687/// | Unknown | Both forms (fallback) |
688///
689/// # Examples
690///
691/// ```rust,ignore
692/// let bools = get_boolean_keywords("python");
693/// assert_eq!(bools.get("True"), Some(&true));
694/// assert_eq!(bools.get("False"), Some(&false));
695///
696/// let bools = get_boolean_keywords("typescript");
697/// assert_eq!(bools.get("true"), Some(&true));
698/// assert_eq!(bools.get("false"), Some(&false));
699/// ```
700pub fn get_boolean_keywords(language: &str) -> HashMap<&'static str, bool> {
701    match language.to_lowercase().as_str() {
702        "python" => [("True", true), ("False", false)].into_iter().collect(),
703        "typescript" | "javascript" | "go" | "rust" | "java" | "kotlin" | "csharp" | "c#"
704        | "swift" => [("true", true), ("false", false)].into_iter().collect(),
705        _ => {
706            // Fallback: accept both forms for unknown languages
707            [
708                ("True", true),
709                ("False", false),
710                ("true", true),
711                ("false", false),
712            ]
713            .into_iter()
714            .collect()
715        }
716    }
717}
718
719/// CAP-AI-17: Get single-line comment pattern for a language.
720///
721/// Returns the string that starts a single-line comment in the given language.
722///
723/// # Language Support Table
724///
725/// | Language | Comment Pattern |
726/// |----------|----------------|
727/// | Python | `#` |
728/// | TypeScript/JavaScript/Go/Rust/Java/C#/Kotlin/Swift | `//` |
729/// | Unknown | `#` (fallback) |
730///
731/// # Note
732///
733/// This only handles single-line comments. Multi-line comments (`/* */`)
734/// are not stripped by this pattern. This is a documented MVP limitation
735/// (TIGER-PASS1-14).
736///
737/// # Examples
738///
739/// ```rust,ignore
740/// let pattern = get_comment_pattern("python");
741/// assert_eq!(pattern, "#");
742///
743/// let pattern = get_comment_pattern("typescript");
744/// assert_eq!(pattern, "//");
745/// ```
746pub fn get_comment_pattern(language: &str) -> &'static str {
747    match language.to_lowercase().as_str() {
748        "python" => "#",
749        "typescript" | "javascript" | "go" | "rust" | "java" | "csharp" | "c#" | "kotlin"
750        | "swift" => "//",
751        _ => "#", // Fallback
752    }
753}
754
755// =============================================================================
756// CAP-AI-14: RHS Parsing for Assignments (Phase 9)
757// =============================================================================
758
759/// Strip single-line comment from end of line.
760///
761/// # TIGER-PASS1-14 Mitigation
762///
763/// Only handles single-line comments (# for Python, // for most others).
764/// Multi-line comments (/* */) are NOT stripped - this is a documented
765/// MVP limitation.
766///
767/// # Arguments
768///
769/// * `line` - Source line to strip comment from
770/// * `language` - Language identifier for comment pattern
771///
772/// # Returns
773///
774/// Line with trailing comment removed
775///
776/// # Examples
777///
778/// ```rust,ignore
779/// let stripped = strip_comment("x = 5  # comment", "python");
780/// assert_eq!(stripped, "x = 5  ");
781///
782/// let stripped = strip_comment("x = 5  // comment", "typescript");
783/// assert_eq!(stripped, "x = 5  ");
784/// ```
785pub fn strip_comment<'a>(line: &'a str, language: &str) -> &'a str {
786    let pattern = get_comment_pattern(language);
787
788    // Handle strings: don't strip if comment marker is inside a string
789    // This is a simplified check - look for comment marker outside quotes
790    let mut in_string = false;
791    let mut string_char: Option<char> = None;
792    let mut escape_next = false;
793
794    for (i, c) in line.char_indices() {
795        if escape_next {
796            escape_next = false;
797            continue;
798        }
799
800        if c == '\\' {
801            escape_next = true;
802            continue;
803        }
804
805        if in_string {
806            if Some(c) == string_char {
807                in_string = false;
808                string_char = None;
809            }
810        } else if c == '"' || c == '\'' {
811            in_string = true;
812            string_char = Some(c);
813        } else if line[i..].starts_with(pattern) {
814            return &line[..i];
815        }
816    }
817
818    line
819}
820
821/// Replace string literal contents with spaces, preserving positions.
822///
823/// Walks the line character-by-character. When inside a quoted string
824/// (`"`, `'`, or backtick), every character (except the delimiters
825/// themselves) is replaced with a space. This prevents text-level scanners
826/// (e.g. `find_div_zero`) from matching operators inside string literals.
827///
828/// Handles escape sequences (`\"`, `\'`, `\\`) and Rust raw strings
829/// (`r"..."`, `r#"..."#`, `r##"..."##`, etc.).
830pub fn strip_strings(line: &str, language: &str) -> String {
831    let bytes = line.as_bytes();
832    let len = bytes.len();
833    let mut result = String::with_capacity(len);
834    let mut i = 0;
835
836    while i < len {
837        let c = bytes[i];
838
839        // --- Rust raw strings: r"...", r#"..."#, r##"..."##, etc. ---
840        if language == "rust" && c == b'r' {
841            // Count hashes after 'r'
842            let mut hashes = 0;
843            let mut j = i + 1;
844            while j < len && bytes[j] == b'#' {
845                hashes += 1;
846                j += 1;
847            }
848            if j < len && bytes[j] == b'"' {
849                // This is a raw string: r#"..."#
850                // Keep the r, hashes, and opening quote as-is
851                for &b in &bytes[i..=j] {
852                    result.push(b as char);
853                }
854                i = j + 1;
855                // Now blank everything until closing: "###
856                let close_start = b'"';
857                loop {
858                    if i >= len {
859                        break; // Unterminated raw string
860                    }
861                    if bytes[i] == close_start {
862                        // Check if followed by the right number of hashes
863                        let mut matched = 0;
864                        let mut k = i + 1;
865                        while k < len && bytes[k] == b'#' && matched < hashes {
866                            matched += 1;
867                            k += 1;
868                        }
869                        if matched == hashes {
870                            // Found the closing delimiter
871                            for &b in &bytes[i..k] {
872                                result.push(b as char);
873                            }
874                            i = k;
875                            break;
876                        }
877                    }
878                    // Inside raw string: blank
879                    result.push(' ');
880                    i += 1;
881                }
882                continue;
883            }
884            // Not a raw string, fall through to normal processing
885        }
886
887        // --- Regular strings: "...", '...', `...` ---
888        if c == b'"' || c == b'\'' || c == b'`' {
889            let delim = c;
890            result.push(c as char);
891            i += 1;
892            while i < len {
893                if bytes[i] == b'\\' {
894                    // Escape: blank both backslash and next char
895                    result.push(' ');
896                    i += 1;
897                    if i < len {
898                        result.push(' ');
899                        i += 1;
900                    }
901                } else if bytes[i] == delim {
902                    // Closing delimiter: keep it
903                    result.push(delim as char);
904                    i += 1;
905                    break;
906                } else {
907                    // Inside string: blank
908                    result.push(' ');
909                    i += 1;
910                }
911            }
912            continue;
913        }
914
915        // --- Normal code: keep as-is ---
916        result.push(c as char);
917        i += 1;
918    }
919
920    result
921}
922
923/// Check if a string is a valid identifier (variable name).
924///
925/// Identifiers start with a letter or underscore, followed by
926/// letters, digits, or underscores.
927///
928/// # Examples
929///
930/// ```rust,ignore
931/// assert!(is_identifier("foo"));
932/// assert!(is_identifier("_bar"));
933/// assert!(is_identifier("var123"));
934/// assert!(!is_identifier("123var"));
935/// assert!(!is_identifier("foo.bar"));
936/// ```
937pub fn is_identifier(s: &str) -> bool {
938    if s.is_empty() {
939        return false;
940    }
941
942    let mut chars = s.chars();
943    match chars.next() {
944        Some(c) if c.is_alphabetic() || c == '_' => {}
945        _ => return false,
946    }
947
948    chars.all(|c| c.is_alphanumeric() || c == '_')
949}
950
951/// Extract RHS from assignment line.
952///
953/// Handles both regular assignments (`var = expr`) and augmented assignments
954/// (`var += expr`, `var -= expr`, etc.).
955///
956/// # TIGER-PASS2-2 Mitigation
957///
958/// Augmented assignments are converted to regular assignment form:
959/// - `x += 5` becomes `x + 5` (as if from `x = x + 5`)
960/// - `x -= 3` becomes `x - 3`
961/// - `x *= 2` becomes `x * 2`
962///
963/// # Arguments
964///
965/// * `line` - Source line containing the assignment
966/// * `var` - Variable being assigned to
967///
968/// # Returns
969///
970/// The RHS expression as a string, or None if not found
971///
972/// # Examples
973///
974/// ```rust,ignore
975/// let rhs = extract_rhs("x = a + b", "x");
976/// assert_eq!(rhs, Some("a + b".to_string()));
977///
978/// let rhs = extract_rhs("x += 5", "x");
979/// assert_eq!(rhs, Some("x + 5".to_string()));
980/// ```
981pub fn extract_rhs(line: &str, var: &str) -> Option<String> {
982    let line = line.trim();
983
984    // Check for augmented assignment first: var += val, var -= val, var *= val
985    let augmented_ops = &[
986        ("+=", '+'),
987        ("-=", '-'),
988        ("*=", '*'),
989        ("/=", '/'),
990        ("%=", '%'),
991    ];
992
993    for (op_str, op_char) in augmented_ops {
994        // Pattern: "var += expr" or "var+= expr" or "var +=expr"
995        let pattern_spaced = format!("{} {} ", var, op_str);
996        let pattern_left_space = format!("{} {}", var, op_str);
997        let pattern_right_space = format!("{}{} ", var, op_str);
998        let pattern_no_space = format!("{}{}", var, op_str);
999
1000        if let Some(idx) = line.find(&pattern_spaced) {
1001            if idx == 0
1002                || !line[..idx]
1003                    .chars()
1004                    .last()
1005                    .map(|c| c.is_alphanumeric() || c == '_')
1006                    .unwrap_or(false)
1007            {
1008                let rhs_start = idx + pattern_spaced.len();
1009                let rhs = line[rhs_start..].trim();
1010                // Convert augmented to: var op rhs
1011                return Some(format!("{} {} {}", var, op_char, rhs));
1012            }
1013        }
1014
1015        if let Some(idx) = line.find(&pattern_left_space) {
1016            if idx == 0
1017                || !line[..idx]
1018                    .chars()
1019                    .last()
1020                    .map(|c| c.is_alphanumeric() || c == '_')
1021                    .unwrap_or(false)
1022            {
1023                let rhs_start = idx + pattern_left_space.len();
1024                let rhs = line[rhs_start..].trim();
1025                return Some(format!("{} {} {}", var, op_char, rhs));
1026            }
1027        }
1028
1029        if let Some(idx) = line.find(&pattern_right_space) {
1030            if idx == 0
1031                || !line[..idx]
1032                    .chars()
1033                    .last()
1034                    .map(|c| c.is_alphanumeric() || c == '_')
1035                    .unwrap_or(false)
1036            {
1037                let rhs_start = idx + pattern_right_space.len();
1038                let rhs = line[rhs_start..].trim();
1039                return Some(format!("{} {} {}", var, op_char, rhs));
1040            }
1041        }
1042
1043        if let Some(idx) = line.find(&pattern_no_space) {
1044            if idx == 0
1045                || !line[..idx]
1046                    .chars()
1047                    .last()
1048                    .map(|c| c.is_alphanumeric() || c == '_')
1049                    .unwrap_or(false)
1050            {
1051                let rhs_start = idx + pattern_no_space.len();
1052                let rhs = line[rhs_start..].trim();
1053                return Some(format!("{} {} {}", var, op_char, rhs));
1054            }
1055        }
1056    }
1057
1058    // Regular assignment: var = expr
1059    // Need to find "var =" or "var=" pattern
1060    let patterns = [
1061        format!("{} = ", var),
1062        format!("{}= ", var),
1063        format!("{} =", var),
1064        format!("{}=", var),
1065    ];
1066
1067    for pattern in &patterns {
1068        if let Some(idx) = line.find(pattern) {
1069            // Make sure we're matching the whole variable name
1070            // Check that character before (if any) is not alphanumeric
1071            let valid_start = idx == 0
1072                || !line[..idx]
1073                    .chars()
1074                    .last()
1075                    .map(|c| c.is_alphanumeric() || c == '_')
1076                    .unwrap_or(false);
1077
1078            if valid_start {
1079                let rhs_start = idx + pattern.len();
1080                return Some(line[rhs_start..].trim().to_string());
1081            }
1082        }
1083    }
1084
1085    // Handle walrus operator for Python (:=)
1086    let walrus_pattern = format!("{} := ", var);
1087    if let Some(idx) = line.find(&walrus_pattern) {
1088        let valid_start = idx == 0
1089            || !line[..idx]
1090                .chars()
1091                .last()
1092                .map(|c| c.is_alphanumeric() || c == '_')
1093                .unwrap_or(false);
1094
1095        if valid_start {
1096            let rhs_start = idx + walrus_pattern.len();
1097            return Some(line[rhs_start..].trim().to_string());
1098        }
1099    }
1100
1101    None
1102}
1103
1104/// Parse simple arithmetic expression: "var op const" or "const op var"
1105///
1106/// # Supported Patterns
1107///
1108/// - `a + 1`, `a - 1`, `a * 2`
1109/// - `1 + a`, `2 * a`
1110///
1111/// # Arguments
1112///
1113/// * `rhs` - Right-hand side expression string
1114///
1115/// # Returns
1116///
1117/// Tuple of (variable_name, operator, constant_value) if pattern matches
1118///
1119/// # Examples
1120///
1121/// ```rust,ignore
1122/// let result = parse_simple_arithmetic("a + 1");
1123/// assert_eq!(result, Some(("a".to_string(), '+', 1)));
1124///
1125/// let result = parse_simple_arithmetic("3 * x");
1126/// assert_eq!(result, Some(("x".to_string(), '*', 3)));
1127/// ```
1128pub fn parse_simple_arithmetic(rhs: &str) -> Option<(String, char, i64)> {
1129    let rhs = rhs.trim();
1130
1131    // Look for arithmetic operators: +, -, *
1132    // Try to parse patterns like: "var + const" or "const + var"
1133    for op in ['+', '-', '*'] {
1134        // Handle both "a + b" and "a+b" formats
1135        let parts: Vec<&str> = if rhs.contains(&format!(" {} ", op)) {
1136            rhs.splitn(2, &format!(" {} ", op)).collect()
1137        } else if rhs.contains(op) {
1138            rhs.splitn(2, op).collect()
1139        } else {
1140            continue;
1141        };
1142
1143        if parts.len() != 2 {
1144            continue;
1145        }
1146
1147        let left = parts[0].trim();
1148        let right = parts[1].trim();
1149
1150        // Try: var op const
1151        if is_identifier(left) {
1152            if let Ok(c) = right.parse::<i64>() {
1153                return Some((left.to_string(), op, c));
1154            }
1155        }
1156
1157        // Try: const op var (only for commutative ops + and *)
1158        if op == '+' || op == '*' {
1159            if let Ok(c) = left.parse::<i64>() {
1160                if is_identifier(right) {
1161                    return Some((right.to_string(), op, c));
1162                }
1163            }
1164        }
1165    }
1166
1167    None
1168}
1169
1170/// Parse RHS of assignment and compute abstract value.
1171///
1172/// # CAP-AI-14: RHS Parsing
1173///
1174/// Handles the following RHS patterns:
1175/// - Integer literals: `x = 5` -> `from_constant(Int(5))`
1176/// - Float literals: `x = 3.14` -> `from_constant(Float(3.14))`
1177/// - String literals: `x = "hello"` or `x = 'hello'` -> `from_constant(String("hello"))`
1178/// - Boolean literals: `x = True/true` -> `from_constant(Bool(true))`
1179/// - Null literals: `x = None/null/nil` -> `from_constant(Null)`
1180/// - Variable copies: `x = y` -> `state.get("y")`
1181/// - Simple arithmetic: `x = a + 1` -> `apply_arithmetic(state.get("a"), '+', 1)`
1182/// - Augmented assignment: `x += 1` treated as `x = x + 1`
1183///
1184/// # TIGER Mitigations
1185///
1186/// - TIGER-PASS2-2: Augmented assignments (+=, -=, *=) converted to regular assignments
1187/// - TIGER-PASS1-14: Only single-line comments are stripped
1188///
1189/// # Arguments
1190///
1191/// * `line` - Source line containing the assignment
1192/// * `var` - Variable being assigned to
1193/// * `state` - Current abstract state (for variable lookups)
1194/// * `language` - Language identifier (for null/boolean keywords)
1195///
1196/// # Returns
1197///
1198/// Abstract value representing the RHS expression
1199///
1200/// # Examples
1201///
1202/// ```rust,ignore
1203/// let state = AbstractState::new();
1204/// let val = parse_rhs_abstract("x = 5", "x", &state, "python");
1205/// assert_eq!(val.range_, Some((Some(5), Some(5))));
1206/// ```
1207pub fn parse_rhs_abstract(
1208    line: &str,
1209    var: &str,
1210    state: &AbstractState,
1211    language: &str,
1212) -> AbstractValue {
1213    // Strip comments first
1214    let line = strip_comment(line, language);
1215
1216    // Extract the RHS expression
1217    let rhs = match extract_rhs(line, var) {
1218        Some(r) => r,
1219        None => return AbstractValue::top(),
1220    };
1221
1222    let rhs = rhs.trim();
1223
1224    // Empty RHS
1225    if rhs.is_empty() {
1226        return AbstractValue::top();
1227    }
1228
1229    // Integer literal (including negative)
1230    if let Ok(v) = rhs.parse::<i64>() {
1231        return AbstractValue::from_constant(ConstantValue::Int(v));
1232    }
1233
1234    // Float literal (including negative)
1235    // Must check after integer to avoid matching "5" as float
1236    if rhs.contains('.') || rhs.to_lowercase().contains('e') {
1237        if let Ok(v) = rhs.parse::<f64>() {
1238            return AbstractValue::from_constant(ConstantValue::Float(v));
1239        }
1240    }
1241
1242    // String literal (double or single quotes)
1243    if (rhs.starts_with('"') && rhs.ends_with('"') && rhs.len() >= 2)
1244        || (rhs.starts_with('\'') && rhs.ends_with('\'') && rhs.len() >= 2)
1245    {
1246        let s = rhs[1..rhs.len() - 1].to_string();
1247        return AbstractValue::from_constant(ConstantValue::String(s));
1248    }
1249
1250    // Triple-quoted strings (Python)
1251    if (rhs.starts_with("\"\"\"") && rhs.ends_with("\"\"\"") && rhs.len() >= 6)
1252        || (rhs.starts_with("'''") && rhs.ends_with("'''") && rhs.len() >= 6)
1253    {
1254        let s = rhs[3..rhs.len() - 3].to_string();
1255        return AbstractValue::from_constant(ConstantValue::String(s));
1256    }
1257
1258    // Null keywords (language-specific via CAP-AI-15)
1259    let null_keywords = get_null_keywords(language);
1260    if null_keywords.contains(&rhs) {
1261        // Handle TypeScript undefined specially (TIGER-PASS2-8)
1262        if rhs == "undefined" {
1263            return AbstractValue {
1264                type_: Some("undefined".to_string()),
1265                range_: None,
1266                nullable: Nullability::Always,
1267                constant: None,
1268            };
1269        }
1270        return AbstractValue::from_constant(ConstantValue::Null);
1271    }
1272
1273    // Boolean keywords (language-specific via CAP-AI-16)
1274    let bool_keywords = get_boolean_keywords(language);
1275    if let Some(&b) = bool_keywords.get(rhs) {
1276        return AbstractValue::from_constant(ConstantValue::Bool(b));
1277    }
1278
1279    // Variable copy: x = y (where y is a simple identifier)
1280    if is_identifier(rhs) {
1281        return state.get(rhs);
1282    }
1283
1284    // Simple arithmetic: x = a + 1 or x = a - 1 (CAP-AI-13)
1285    if let Some((operand_var, op, constant)) = parse_simple_arithmetic(rhs) {
1286        let operand_value = state.get(&operand_var);
1287        return apply_arithmetic(&operand_value, op, constant);
1288    }
1289
1290    // Unknown RHS - return top (unknown)
1291    AbstractValue::top()
1292}
1293
1294// =============================================================================
1295// CAP-AI-13: Abstract Arithmetic Operations (Phase 7)
1296// =============================================================================
1297
1298/// Apply arithmetic operation to abstract value.
1299///
1300/// # CRITICAL: TIGER-PASS1-11 Mitigation
1301///
1302/// Uses saturating arithmetic to prevent overflow panic.
1303/// On overflow, the bound is widened to unbounded (None).
1304///
1305/// # Supported Operations
1306///
1307/// - `'+'`: Addition - adds constant to both bounds
1308/// - `'-'`: Subtraction - subtracts constant from both bounds
1309/// - `'*'`: Multiplication - multiplies bounds by constant (handles sign changes)
1310///
1311/// # Examples
1312///
1313/// ```rust,ignore
1314/// let val = AbstractValue::from_constant(ConstantValue::Int(5));
1315/// let result = apply_arithmetic(&val, '+', 3);
1316/// // result.range_ == Some((Some(8), Some(8)))
1317///
1318/// let range_val = AbstractValue {
1319///     type_: Some("int".to_string()),
1320///     range_: Some((Some(1), Some(5))),
1321///     nullable: Nullability::Never,
1322///     constant: None,
1323/// };
1324/// let result = apply_arithmetic(&range_val, '+', 10);
1325/// // result.range_ == Some((Some(11), Some(15)))
1326/// ```
1327///
1328/// # Overflow Handling
1329///
1330/// When saturating arithmetic reaches i64::MAX or i64::MIN, the bound
1331/// is widened to None (unbounded) to maintain soundness:
1332///
1333/// ```rust,ignore
1334/// let max_val = AbstractValue::from_constant(ConstantValue::Int(i64::MAX));
1335/// let result = apply_arithmetic(&max_val, '+', 1);
1336/// // result.range_ contains None bounds - widened to unbounded
1337/// ```
1338pub fn apply_arithmetic(operand: &AbstractValue, op: char, constant: i64) -> AbstractValue {
1339    let new_range = operand.range_.map(|(low, high)| {
1340        match op {
1341            '+' => {
1342                // TIGER-PASS1-11: Use saturating_add
1343                let new_low = low.and_then(|l| {
1344                    let result = l.saturating_add(constant);
1345                    // If saturated to MAX/MIN, widen to unbounded
1346                    if (constant > 0 && result == i64::MAX && l != i64::MAX - constant)
1347                        || (constant < 0 && result == i64::MIN && l != i64::MIN - constant)
1348                    {
1349                        return None;
1350                    }
1351                    Some(result)
1352                });
1353
1354                let new_high = high.and_then(|h| {
1355                    let result = h.saturating_add(constant);
1356                    // If saturated to MAX/MIN, widen to unbounded
1357                    if (constant > 0 && result == i64::MAX && h != i64::MAX - constant)
1358                        || (constant < 0 && result == i64::MIN && h != i64::MIN - constant)
1359                    {
1360                        return None;
1361                    }
1362                    Some(result)
1363                });
1364
1365                (new_low, new_high)
1366            }
1367            '-' => {
1368                // TIGER-PASS1-11: Use saturating_sub
1369                let new_low = low.and_then(|l| {
1370                    let result = l.saturating_sub(constant);
1371                    // If saturated to MAX/MIN, widen to unbounded
1372                    if (constant > 0 && result == i64::MIN && l != i64::MIN + constant)
1373                        || (constant < 0 && result == i64::MAX && l != i64::MAX + constant)
1374                    {
1375                        return None;
1376                    }
1377                    Some(result)
1378                });
1379
1380                let new_high = high.and_then(|h| {
1381                    let result = h.saturating_sub(constant);
1382                    // If saturated to MAX/MIN, widen to unbounded
1383                    if (constant > 0 && result == i64::MIN && h != i64::MIN + constant)
1384                        || (constant < 0 && result == i64::MAX && h != i64::MAX + constant)
1385                    {
1386                        return None;
1387                    }
1388                    Some(result)
1389                });
1390
1391                (new_low, new_high)
1392            }
1393            '*' => {
1394                // TIGER-PASS1-11: Handle sign changes for multiplication
1395                // When multiplying by negative, low and high swap
1396                // Use saturating_mul and detect overflow
1397
1398                let compute_mul = |bound: Option<i64>| -> Option<i64> {
1399                    bound.and_then(|b| {
1400                        // Check for overflow before multiplying
1401                        if constant == 0 {
1402                            return Some(0);
1403                        }
1404                        // Use checked_mul to detect overflow (None = overflow -> unbounded)
1405                        b.checked_mul(constant)
1406                    })
1407                };
1408
1409                let low_mul = compute_mul(low);
1410                let high_mul = compute_mul(high);
1411
1412                // When multiplying by negative constant, bounds swap
1413                if constant < 0 {
1414                    (high_mul, low_mul)
1415                } else if constant == 0 {
1416                    // Multiplying by zero gives exact [0, 0]
1417                    (Some(0), Some(0))
1418                } else {
1419                    (low_mul, high_mul)
1420                }
1421            }
1422            _ => {
1423                // Unknown operator -> widen to unbounded
1424                (None, None)
1425            }
1426        }
1427    });
1428
1429    // Determine if result is still a constant
1430    let new_constant = if operand.is_constant() {
1431        if let Some((Some(l), Some(h))) = new_range {
1432            if l == h {
1433                Some(ConstantValue::Int(l))
1434            } else {
1435                None
1436            }
1437        } else {
1438            None
1439        }
1440    } else {
1441        None
1442    };
1443
1444    AbstractValue {
1445        type_: operand.type_.clone(),
1446        range_: new_range,
1447        nullable: operand.nullable,
1448        constant: new_constant,
1449    }
1450}
1451
1452// =============================================================================
1453// CAP-AI-08: Join Operations (Phase 6)
1454// =============================================================================
1455
1456/// Join two abstract values at a merge point.
1457///
1458/// Combines two abstract values by taking the least upper bound:
1459/// - Ranges: union bounds -> [min(low1, low2), max(high1, high2)]
1460/// - Constants: lose if disagree, keep if same
1461/// - Nullability: NEVER + NEVER = NEVER, else MAYBE
1462/// - Types: lose if disagree, keep if same
1463///
1464/// # Examples
1465///
1466/// ```rust,ignore
1467/// // Range union
1468/// let val1 = AbstractValue { range_: Some((Some(1), Some(1))), .. };
1469/// let val2 = AbstractValue { range_: Some((Some(10), Some(10))), .. };
1470/// let joined = join_values(&val1, &val2);
1471/// assert_eq!(joined.range_, Some((Some(1), Some(10))));
1472/// ```
1473pub fn join_values(a: &AbstractValue, b: &AbstractValue) -> AbstractValue {
1474    // Range: union (widest bounds)
1475    let joined_range = match (&a.range_, &b.range_) {
1476        (None, None) => None,
1477        (Some(r), None) | (None, Some(r)) => Some(*r),
1478        (Some((a_low, a_high)), Some((b_low, b_high))) => {
1479            // Take minimum of lows and maximum of highs
1480            let low = match (a_low, b_low) {
1481                (None, _) | (_, None) => None,
1482                (Some(a), Some(b)) => Some(std::cmp::min(*a, *b)),
1483            };
1484            let high = match (a_high, b_high) {
1485                (None, _) | (_, None) => None,
1486                (Some(a), Some(b)) => Some(std::cmp::max(*a, *b)),
1487            };
1488            Some((low, high))
1489        }
1490    };
1491
1492    // Type: common type or None
1493    let joined_type = if a.type_ == b.type_ {
1494        a.type_.clone()
1495    } else {
1496        None
1497    };
1498
1499    // Nullable: NEVER only if both are NEVER, else MAYBE
1500    let joined_nullable = match (a.nullable, b.nullable) {
1501        (Nullability::Never, Nullability::Never) => Nullability::Never,
1502        (Nullability::Always, Nullability::Always) => Nullability::Always,
1503        _ => Nullability::Maybe,
1504    };
1505
1506    // Constant: only if both have same constant
1507    let joined_constant = match (&a.constant, &b.constant) {
1508        (Some(ca), Some(cb)) if ca == cb => Some(ca.clone()),
1509        _ => None,
1510    };
1511
1512    AbstractValue {
1513        type_: joined_type,
1514        range_: joined_range,
1515        nullable: joined_nullable,
1516        constant: joined_constant,
1517    }
1518}
1519
1520/// Join multiple abstract states at a CFG merge point.
1521///
1522/// For each variable present in any input state:
1523///   result[var] = join of all values for var
1524///
1525/// Variables not present in a state are treated as `top()`.
1526///
1527/// # Arguments
1528///
1529/// * `states` - Slice of references to states to join
1530///
1531/// # Returns
1532///
1533/// New state containing joined values for all variables
1534pub fn join_states(states: &[&AbstractState]) -> AbstractState {
1535    if states.is_empty() {
1536        return AbstractState::default();
1537    }
1538    if states.len() == 1 {
1539        return states[0].clone();
1540    }
1541
1542    // Collect all variable names from all states
1543    let all_vars: std::collections::HashSet<_> = states
1544        .iter()
1545        .flat_map(|s| s.values.keys().cloned())
1546        .collect();
1547
1548    let mut result = HashMap::new();
1549    for var in all_vars {
1550        // Get values from all states (top() for missing)
1551        let values: Vec<AbstractValue> = states.iter().map(|s| s.get(&var)).collect();
1552
1553        // Join all values pairwise
1554        let mut joined = values[0].clone();
1555        for val in values.iter().skip(1) {
1556            joined = join_values(&joined, val);
1557        }
1558        result.insert(var, joined);
1559    }
1560
1561    AbstractState { values: result }
1562}
1563
1564// =============================================================================
1565// CAP-AI-09: Widening Operations (Phase 6)
1566// =============================================================================
1567
1568/// Widen a value to ensure termination on loops.
1569///
1570/// Compares old and new values and widens bounds that are growing:
1571/// - If new.low < old.low, widen low to None (negative infinity)
1572/// - If new.high > old.high, widen high to None (positive infinity)
1573/// - Constant information is always lost on widening
1574///
1575/// # Arguments
1576///
1577/// * `old` - Value from previous iteration
1578/// * `new` - Value from current iteration
1579///
1580/// # Returns
1581///
1582/// Widened value that ensures fixpoint convergence
1583pub fn widen_value(old: &AbstractValue, new: &AbstractValue) -> AbstractValue {
1584    let widened_range = match (&old.range_, &new.range_) {
1585        (None, None) => None,
1586        (None, r) => *r,
1587        (_, None) => None, // New has unbounded range, keep it
1588        (Some((old_low, old_high)), Some((new_low, new_high))) => {
1589            // Widen low: if growing downward (more negative), widen to -inf
1590            let widened_low = match (old_low, new_low) {
1591                (None, _) => None,                     // Already widened
1592                (_, None) => None,                     // Widen to -inf
1593                (Some(o), Some(n)) if *n < *o => None, // Growing down -> widen
1594                (_, n) => *n,                          // Not growing, keep new value
1595            };
1596
1597            // Widen high: if growing upward (more positive), widen to +inf
1598            let widened_high = match (old_high, new_high) {
1599                (None, _) => None,                     // Already widened
1600                (_, None) => None,                     // Widen to +inf
1601                (Some(o), Some(n)) if *n > *o => None, // Growing up -> widen
1602                (_, n) => *n,                          // Not growing, keep new value
1603            };
1604
1605            Some((widened_low, widened_high))
1606        }
1607    };
1608
1609    AbstractValue {
1610        type_: new.type_.clone(),
1611        range_: widened_range,
1612        nullable: new.nullable,
1613        constant: None, // CAP-AI-09: Constant lost after widening
1614    }
1615}
1616
1617/// Widen state at loop headers to ensure termination.
1618///
1619/// Applies widening to each variable present in either state.
1620///
1621/// # Arguments
1622///
1623/// * `old` - State from previous iteration
1624/// * `new` - State from current iteration
1625///
1626/// # Returns
1627///
1628/// Widened state
1629pub fn widen_state(old: &AbstractState, new: &AbstractState) -> AbstractState {
1630    // Collect all variable names from both states
1631    let all_vars: std::collections::HashSet<_> = old
1632        .values
1633        .keys()
1634        .chain(new.values.keys())
1635        .cloned()
1636        .collect();
1637
1638    let mut result = HashMap::new();
1639    for var in all_vars {
1640        let old_val = old.get(&var);
1641        let new_val = new.get(&var);
1642        result.insert(var, widen_value(&old_val, &new_val));
1643    }
1644
1645    AbstractState { values: result }
1646}
1647
1648// =============================================================================
1649// CAP-AI: Main Algorithm - compute_abstract_interp (Phase 10)
1650// =============================================================================
1651
1652use super::types::{
1653    build_predecessors, find_back_edges, reverse_postorder, validate_cfg, DataflowError,
1654};
1655use crate::types::{CfgInfo, DfgInfo, RefType, VarRef};
1656
1657/// Initialize parameter values as top() (unknown).
1658///
1659/// Parameters are identified from VarRefs as definitions in the entry block
1660/// that appear without prior use (typical function parameter pattern).
1661///
1662/// All parameters start as top() because we don't know the caller's values.
1663///
1664/// # Arguments
1665///
1666/// * `cfg` - Control flow graph with entry block info
1667/// * `dfg` - Data flow graph with variable references
1668///
1669/// # Returns
1670///
1671/// AbstractState with all parameters set to top()
1672pub fn init_params(cfg: &CfgInfo, dfg: &DfgInfo) -> AbstractState {
1673    let mut state = AbstractState::new();
1674
1675    // Find the entry block
1676    let entry_block = cfg.blocks.iter().find(|b| b.id == cfg.entry_block);
1677
1678    if let Some(entry) = entry_block {
1679        // Find all definitions in the entry block
1680        // Parameters are typically defined at the start of the function
1681        for var_ref in &dfg.refs {
1682            // A definition in the entry block with no prior use is likely a parameter
1683            if var_ref.ref_type == RefType::Definition {
1684                // Check if this line is within the entry block
1685                if var_ref.line >= entry.lines.0 && var_ref.line <= entry.lines.1 {
1686                    // Initialize as top (unknown value from caller)
1687                    state
1688                        .values
1689                        .insert(var_ref.name.clone(), AbstractValue::top());
1690                }
1691            }
1692        }
1693    }
1694
1695    state
1696}
1697
1698/// Transfer function: update state based on block operations.
1699///
1700/// Processes all statements in a block and updates the abstract state.
1701/// Each assignment updates the corresponding variable's abstract value.
1702///
1703/// # Algorithm
1704///
1705/// For each VarRef of type Def in the block:
1706/// 1. Get the source line for this definition
1707/// 2. Parse the RHS to compute the new abstract value
1708/// 3. Update the state with the new value
1709///
1710/// # Arguments
1711///
1712/// * `state` - Abstract state at block entry
1713/// * `block` - CFG block being processed
1714/// * `dfg` - Data flow graph with variable references
1715/// * `source_lines` - Optional source code lines for RHS parsing
1716/// * `language` - Language identifier for keyword recognition
1717///
1718/// # Returns
1719///
1720/// New AbstractState at block exit
1721pub fn transfer_block(
1722    state: &AbstractState,
1723    block: &crate::types::CfgBlock,
1724    dfg: &DfgInfo,
1725    source_lines: Option<&[&str]>,
1726    language: &str,
1727) -> AbstractState {
1728    let mut current_state = state.clone();
1729
1730    // Get all definitions in this block, sorted by line
1731    let mut defs_in_block: Vec<&VarRef> = dfg
1732        .refs
1733        .iter()
1734        .filter(|r| {
1735            r.ref_type == RefType::Definition && r.line >= block.lines.0 && r.line <= block.lines.1
1736        })
1737        .collect();
1738
1739    // Sort by line number for correct order of operations
1740    defs_in_block.sort_by_key(|r| (r.line, r.column));
1741
1742    // Process each definition in order
1743    for var_ref in defs_in_block {
1744        // Get source line if available
1745        let new_value = if let Some(lines) = source_lines {
1746            // Convert 1-based line to 0-based index
1747            let line_idx = var_ref.line.saturating_sub(1) as usize;
1748            if line_idx < lines.len() {
1749                let line = lines[line_idx];
1750                parse_rhs_abstract(line, &var_ref.name, &current_state, language)
1751            } else {
1752                AbstractValue::top()
1753            }
1754        } else {
1755            // No source available - default to top
1756            AbstractValue::top()
1757        };
1758
1759        // Update state
1760        current_state = current_state.set(&var_ref.name, new_value);
1761    }
1762
1763    current_state
1764}
1765
1766// =============================================================================
1767// Phase 11: Safety Check Detection (CAP-AI-10, CAP-AI-11, CAP-AI-20)
1768// =============================================================================
1769
1770/// Find potential division-by-zero based on range analysis.
1771///
1772/// CRITICAL: Intra-block precision (TIGER-PASS1-13)
1773/// For division at line L in block B:
1774///   1. Find all defs before L in same block
1775///   2. If divisor defined before L, use state after that def
1776///   3. Else use state_in[B]
1777///
1778/// # Arguments
1779///
1780/// * `cfg` - Control flow graph
1781/// * `dfg` - Data flow graph with variable references
1782/// * `state_in` - Abstract state at block entries
1783/// * `source_lines` - Source code lines for division detection
1784/// * `state_out` - Abstract state at block exits
1785///
1786/// # Returns
1787///
1788/// Vec<(line, var)> where divisor may_be_zero()
1789///
1790/// # Algorithm (CAP-AI-20)
1791///
1792/// 1. Scan source lines for division patterns (/, //, %)
1793/// 2. For each division, extract the divisor variable
1794/// 3. Find the containing block and compute state at division point:
1795///    - If divisor is defined before division line in same block, re-compute
1796///      the state up to that point
1797///    - Otherwise, use state_in[block]
1798/// 4. If divisor.may_be_zero(), add warning
1799///
1800/// # ELEPHANT-PASS2-5
1801///
1802/// Limitation: Only direct variable divisors are tracked.
1803/// Complex expressions like `1/(x+y)` are NOT detected.
1804pub fn find_div_zero(
1805    cfg: &CfgInfo,
1806    dfg: &DfgInfo,
1807    state_in: &HashMap<BlockId, AbstractState>,
1808    source_lines: Option<&[&str]>,
1809    _state_out: &HashMap<BlockId, AbstractState>,
1810    language: &str,
1811) -> Vec<(usize, String)> {
1812    let mut warnings = Vec::new();
1813
1814    let Some(lines) = source_lines else {
1815        return warnings;
1816    };
1817
1818    // Division operators by language
1819    let div_patterns: &[&str] = match language {
1820        "python" => &["/", "//", "%"],
1821        "rust" | "go" | "typescript" | "javascript" | "java" | "c" | "cpp" => &["/", "%"],
1822        _ => &["/", "%"],
1823    };
1824
1825    // Process each line looking for divisions
1826    for (line_idx, line) in lines.iter().enumerate() {
1827        let line_num = line_idx + 1; // 1-based
1828
1829        // Skip comments, then blank string literal contents so `/` in
1830        // paths like "/src/main.rs" is not mistaken for division.
1831        let code_no_comments = strip_comment(line, language);
1832        let code = strip_strings(code_no_comments, language);
1833
1834        // Check for division operators
1835        for &op in div_patterns {
1836            // Find all occurrences of the division operator
1837            let mut search_start = 0;
1838            while let Some(pos) = code[search_start..].find(op) {
1839                let actual_pos = search_start + pos;
1840
1841                // Skip if this is // for integer division and we're at first /
1842                if op == "/" && code.len() > actual_pos + 1 {
1843                    let next_char = code.chars().nth(actual_pos + 1);
1844                    if next_char == Some('/') {
1845                        // This is // (floor division in Python or comment)
1846                        search_start = actual_pos + 2;
1847                        continue;
1848                    }
1849                    // Check if this is part of // that we should handle
1850                    if actual_pos > 0 && code.chars().nth(actual_pos - 1) == Some('/') {
1851                        search_start = actual_pos + 1;
1852                        continue;
1853                    }
1854                }
1855
1856                // Extract the divisor (RHS of division)
1857                let after_op = &code[actual_pos + op.len()..];
1858                let divisor = extract_divisor(after_op.trim());
1859
1860                if let Some(div_var) = divisor {
1861                    if is_identifier(&div_var) {
1862                        // Find which block contains this line
1863                        let block = cfg
1864                            .blocks
1865                            .iter()
1866                            .find(|b| line_num as u32 >= b.lines.0 && line_num as u32 <= b.lines.1);
1867
1868                        if let Some(block) = block {
1869                            // Intra-block precision: compute state at division point
1870                            let state_at_div = compute_state_at_line(
1871                                block,
1872                                dfg,
1873                                state_in.get(&block.id).cloned().unwrap_or_default(),
1874                                source_lines,
1875                                line_num,
1876                                language,
1877                            );
1878
1879                            let divisor_val = state_at_div.get(&div_var);
1880                            if divisor_val.may_be_zero() {
1881                                warnings.push((line_num, div_var));
1882                            }
1883                        }
1884                    }
1885                }
1886
1887                search_start = actual_pos + op.len();
1888            }
1889        }
1890    }
1891
1892    // Deduplicate warnings (same line might have multiple divisions)
1893    warnings.sort();
1894    warnings.dedup();
1895
1896    warnings
1897}
1898
1899/// Extract divisor variable from expression after division operator.
1900///
1901/// Handles simple cases like: `/ x`, `/ y)`, `/ (a + b)`
1902/// Only returns identifiers (variables), not complex expressions.
1903fn extract_divisor(s: &str) -> Option<String> {
1904    let s = s.trim();
1905    if s.is_empty() {
1906        return None;
1907    }
1908
1909    // Collect identifier characters
1910    let mut chars = s.chars().peekable();
1911
1912    // Skip leading parenthesis if present (we can't handle complex expressions)
1913    if chars.peek() == Some(&'(') {
1914        return None;
1915    }
1916
1917    let mut ident = String::new();
1918    while let Some(&c) = chars.peek() {
1919        if c.is_alphanumeric() || c == '_' {
1920            ident.push(c);
1921            chars.next();
1922        } else {
1923            break;
1924        }
1925    }
1926
1927    if ident.is_empty() || ident.chars().next().unwrap().is_ascii_digit() {
1928        // Not a valid identifier (empty or starts with digit)
1929        // Note: numeric literals are handled conservatively (may_be_zero returns true for unknown)
1930        None
1931    } else {
1932        Some(ident)
1933    }
1934}
1935
1936/// Compute abstract state at a specific line within a block.
1937///
1938/// This provides intra-block precision by replaying the transfer function
1939/// only up to the specified line.
1940fn compute_state_at_line(
1941    block: &crate::types::CfgBlock,
1942    dfg: &DfgInfo,
1943    state_in: AbstractState,
1944    source_lines: Option<&[&str]>,
1945    target_line: usize,
1946    language: &str,
1947) -> AbstractState {
1948    let mut current_state = state_in;
1949
1950    // Get all definitions in this block, sorted by line
1951    let mut defs_in_block: Vec<&VarRef> = dfg
1952        .refs
1953        .iter()
1954        .filter(|r| {
1955            r.ref_type == RefType::Definition
1956                && r.line >= block.lines.0
1957                && r.line <= block.lines.1
1958                && (r.line as usize) < target_line // Only process defs BEFORE target line
1959        })
1960        .collect();
1961
1962    // Sort by line number for correct order of operations
1963    defs_in_block.sort_by_key(|r| (r.line, r.column));
1964
1965    // Process each definition in order
1966    for var_ref in defs_in_block {
1967        // Get source line if available
1968        let new_value = if let Some(lines) = source_lines {
1969            // Convert 1-based line to 0-based index
1970            let line_idx = var_ref.line.saturating_sub(1) as usize;
1971            if line_idx < lines.len() {
1972                let line = lines[line_idx];
1973                parse_rhs_abstract(line, &var_ref.name, &current_state, language)
1974            } else {
1975                AbstractValue::top()
1976            }
1977        } else {
1978            AbstractValue::top()
1979        };
1980
1981        // Update state
1982        current_state = current_state.set(&var_ref.name, new_value);
1983    }
1984
1985    current_state
1986}
1987
1988/// Find potential null dereferences at attribute access.
1989///
1990/// Looks for patterns: var.attr, var.method(), var[idx]
1991/// Checks if var.may_be_null() at that point.
1992///
1993/// # Arguments
1994///
1995/// * `cfg` - Control flow graph
1996/// * `dfg` - Data flow graph with variable references
1997/// * `state_in` - Abstract state at block entries
1998/// * `source_lines` - Source code lines for pattern detection
1999///
2000/// # Returns
2001///
2002/// Vec<(line, var)> where var may be null at dereference point
2003pub fn find_null_deref(
2004    cfg: &CfgInfo,
2005    dfg: &DfgInfo,
2006    state_in: &HashMap<BlockId, AbstractState>,
2007    source_lines: Option<&[&str]>,
2008    language: &str,
2009) -> Vec<(usize, String)> {
2010    let mut warnings = Vec::new();
2011
2012    let Some(lines) = source_lines else {
2013        return warnings;
2014    };
2015
2016    // Process each line looking for attribute access patterns
2017    for (line_idx, line) in lines.iter().enumerate() {
2018        let line_num = line_idx + 1; // 1-based
2019
2020        // Skip comments
2021        let code = strip_comment(line, language);
2022
2023        // Find all attribute access patterns: identifier followed by .
2024        // Pattern: word.something or word[something]
2025        let patterns = extract_deref_patterns(code);
2026
2027        for var in patterns {
2028            if is_identifier(&var) {
2029                // Find which block contains this line
2030                let block = cfg
2031                    .blocks
2032                    .iter()
2033                    .find(|b| line_num as u32 >= b.lines.0 && line_num as u32 <= b.lines.1);
2034
2035                if let Some(block) = block {
2036                    // Intra-block precision: compute state at dereference point
2037                    let state_at_deref = compute_state_at_line(
2038                        block,
2039                        dfg,
2040                        state_in.get(&block.id).cloned().unwrap_or_default(),
2041                        source_lines,
2042                        line_num,
2043                        language,
2044                    );
2045
2046                    let var_val = state_at_deref.get(&var);
2047                    if var_val.may_be_null() {
2048                        warnings.push((line_num, var));
2049                    }
2050                }
2051            }
2052        }
2053    }
2054
2055    // Deduplicate warnings
2056    warnings.sort();
2057    warnings.dedup();
2058
2059    warnings
2060}
2061
2062/// Extract variables being dereferenced from a line of code.
2063///
2064/// Looks for patterns like:
2065/// - `x.foo` -> returns "x"
2066/// - `x.method()` -> returns "x"
2067/// - `x[idx]` -> returns "x"
2068/// - `obj.attr.nested` -> returns "obj"
2069fn extract_deref_patterns(code: &str) -> Vec<String> {
2070    let mut patterns = Vec::new();
2071    let chars: Vec<char> = code.chars().collect();
2072    let len = chars.len();
2073    let mut i = 0;
2074
2075    while i < len {
2076        // Skip non-identifier characters
2077        while i < len && !chars[i].is_alphabetic() && chars[i] != '_' {
2078            i += 1;
2079        }
2080
2081        if i >= len {
2082            break;
2083        }
2084
2085        // Collect identifier
2086        let start = i;
2087        while i < len && (chars[i].is_alphanumeric() || chars[i] == '_') {
2088            i += 1;
2089        }
2090
2091        let ident: String = chars[start..i].iter().collect();
2092
2093        // Check if followed by . or [
2094        if i < len && (chars[i] == '.' || chars[i] == '[') {
2095            // This is a dereference pattern
2096            if !ident.is_empty() && !ident.chars().next().unwrap().is_ascii_digit() {
2097                // Skip keywords that look like dereferences
2098                let keywords = ["self", "this", "super", "cls"];
2099                if !keywords.contains(&ident.as_str()) {
2100                    patterns.push(ident);
2101                }
2102            }
2103        }
2104    }
2105
2106    patterns
2107}
2108
2109/// Compute abstract interpretation with widening for loop termination.
2110///
2111/// # Algorithm
2112///
2113/// 1. Initialize entry block with parameters as top()
2114/// 2. Initialize all other blocks as empty state (unreached)
2115/// 3. Iterate in reverse postorder until fixpoint:
2116///    - state_in[b] = join(state_out[p] for p in preds[b])
2117///    - Apply widening at loop headers (back-edge targets)
2118///    - state_out[b] = transfer(state_in[b], block[b])
2119/// 4. Return AbstractInterpInfo
2120///
2121/// # TIGER Mitigations
2122///
2123/// - TIGER-PASS1-7: Use blocks * 10 + 100 as iteration bound
2124/// - TIGER-PASS3-2: Both analyses take &DfgInfo (unified interface)
2125///
2126/// # Arguments
2127///
2128/// * `cfg` - Control flow graph
2129/// * `dfg` - Data flow graph with variable references
2130/// * `source_lines` - Optional source code lines for RHS parsing
2131/// * `language` - Language identifier (e.g., "python", "typescript", "go")
2132///
2133/// # Returns
2134///
2135/// AbstractInterpInfo containing:
2136/// - state_in: Abstract state at entry of each block
2137/// - state_out: Abstract state at exit of each block
2138/// - potential_div_zero: (line, var) pairs where division by zero is possible
2139/// - potential_null_deref: (line, var) pairs where null dereference is possible
2140///
2141/// # Errors
2142///
2143/// Returns DataflowError if:
2144/// - CFG is empty
2145/// - CFG exceeds MAX_BLOCKS
2146///
2147/// # Example
2148///
2149/// ```rust,ignore
2150/// let result = compute_abstract_interp(&cfg, &dfg, Some(&source_lines), "python")?;
2151///
2152/// // Check for potential issues
2153/// for (line, var) in &result.potential_div_zero {
2154///     println!("Warning: potential div-by-zero at line {}: {}", line, var);
2155/// }
2156/// ```
2157pub fn compute_abstract_interp(
2158    cfg: &CfgInfo,
2159    dfg: &DfgInfo,
2160    source_lines: Option<&[&str]>,
2161    language: &str,
2162) -> Result<AbstractInterpInfo, DataflowError> {
2163    // Validate CFG
2164    validate_cfg(cfg)?;
2165
2166    // Build helper structures
2167    let predecessors = build_predecessors(cfg);
2168    let loop_headers = find_back_edges(cfg);
2169    let block_order = reverse_postorder(cfg);
2170
2171    // Initialize states
2172    let mut state_in: HashMap<BlockId, AbstractState> = HashMap::new();
2173    let mut state_out: HashMap<BlockId, AbstractState> = HashMap::new();
2174
2175    let entry = cfg.entry_block;
2176
2177    // Entry block starts with parameters as top
2178    let init_state = init_params(cfg, dfg);
2179    state_in.insert(entry, init_state.clone());
2180
2181    // Process entry block to get initial state_out
2182    if let Some(entry_block) = cfg.blocks.iter().find(|b| b.id == entry) {
2183        let entry_out = transfer_block(&init_state, entry_block, dfg, source_lines, language);
2184        state_out.insert(entry, entry_out);
2185    } else {
2186        state_out.insert(entry, init_state);
2187    }
2188
2189    // Initialize other blocks as empty (bottom/unreached)
2190    for block in &cfg.blocks {
2191        if block.id != entry {
2192            state_in.insert(block.id, AbstractState::default());
2193            state_out.insert(block.id, AbstractState::default());
2194        }
2195    }
2196
2197    // TIGER-PASS1-7: Iteration bound
2198    let max_iterations = cfg.blocks.len() * 10 + 100;
2199    let mut iteration = 0;
2200    let mut changed = true;
2201
2202    // Fixpoint iteration
2203    while changed && iteration < max_iterations {
2204        changed = false;
2205        iteration += 1;
2206
2207        for &block_id in &block_order {
2208            // Skip entry block (already initialized)
2209            if block_id == entry {
2210                continue;
2211            }
2212
2213            // Find the block
2214            let block = match cfg.blocks.iter().find(|b| b.id == block_id) {
2215                Some(b) => b,
2216                None => continue,
2217            };
2218
2219            // Get predecessors
2220            let preds = predecessors.get(&block_id).cloned().unwrap_or_default();
2221
2222            // Compute new state_in as join of all predecessor state_outs
2223            let mut new_in = if preds.is_empty() {
2224                AbstractState::default()
2225            } else {
2226                // Collect predecessor states
2227                let pred_states: Vec<&AbstractState> =
2228                    preds.iter().filter_map(|p| state_out.get(p)).collect();
2229
2230                if pred_states.is_empty() {
2231                    AbstractState::default()
2232                } else {
2233                    join_states(&pred_states)
2234                }
2235            };
2236
2237            // Apply widening at loop headers (CAP-AI-09)
2238            if loop_headers.contains(&block_id) {
2239                if let Some(old_in) = state_in.get(&block_id) {
2240                    new_in = widen_state(old_in, &new_in);
2241                }
2242            }
2243
2244            // Apply transfer function
2245            let new_out = transfer_block(&new_in, block, dfg, source_lines, language);
2246
2247            // Check for changes
2248            let old_in = state_in.get(&block_id);
2249            let old_out = state_out.get(&block_id);
2250
2251            if old_in != Some(&new_in) || old_out != Some(&new_out) {
2252                changed = true;
2253                state_in.insert(block_id, new_in);
2254                state_out.insert(block_id, new_out);
2255            }
2256        }
2257    }
2258
2259    // Phase 11: Detect potential safety issues
2260    let potential_div_zero = find_div_zero(cfg, dfg, &state_in, source_lines, &state_out, language);
2261    let potential_null_deref = find_null_deref(cfg, dfg, &state_in, source_lines, language);
2262
2263    // Build result
2264    Ok(AbstractInterpInfo {
2265        state_in,
2266        state_out,
2267        potential_div_zero,
2268        potential_null_deref,
2269        function_name: cfg.function.clone(),
2270    })
2271}
2272
2273// =============================================================================
2274// Unit Tests
2275// =============================================================================
2276
2277#[cfg(test)]
2278mod tests {
2279    use super::*;
2280    use std::collections::hash_map::DefaultHasher;
2281    use std::f64::consts::PI;
2282
2283    // =========================================================================
2284    // Nullability Tests (CAP-AI-01)
2285    // =========================================================================
2286
2287    #[test]
2288    fn test_nullability_enum_has_three_values() {
2289        // CAP-AI-01: Nullability has exactly three values
2290        let _never = Nullability::Never;
2291        let _maybe = Nullability::Maybe;
2292        let _always = Nullability::Always;
2293
2294        // Test string representation
2295        assert_eq!(Nullability::Never.as_str(), "never");
2296        assert_eq!(Nullability::Maybe.as_str(), "maybe");
2297        assert_eq!(Nullability::Always.as_str(), "always");
2298    }
2299
2300    #[test]
2301    fn test_nullability_default_is_maybe() {
2302        // CAP-AI-01: Default is Maybe
2303        let default: Nullability = Default::default();
2304        assert_eq!(default, Nullability::Maybe);
2305    }
2306
2307    // =========================================================================
2308    // AbstractValue Tests (CAP-AI-02 to CAP-AI-06)
2309    // =========================================================================
2310
2311    #[test]
2312    fn test_abstract_value_has_required_fields() {
2313        // CAP-AI-02: AbstractValue has type_, range_, nullable, constant
2314        let value = AbstractValue {
2315            type_: Some("int".to_string()),
2316            range_: Some((Some(1), Some(10))),
2317            nullable: Nullability::Never,
2318            constant: Some(ConstantValue::Int(5)),
2319        };
2320
2321        assert_eq!(value.type_, Some("int".to_string()));
2322        assert_eq!(value.range_, Some((Some(1), Some(10))));
2323        assert_eq!(value.nullable, Nullability::Never);
2324        assert!(value.constant.is_some());
2325    }
2326
2327    #[test]
2328    fn test_abstract_value_is_hashable() {
2329        // CAP-AI-02: AbstractValue must be hashable
2330        let value1 = AbstractValue::from_constant(ConstantValue::Int(5));
2331        let value2 = AbstractValue::from_constant(ConstantValue::Int(5));
2332
2333        let mut hasher1 = DefaultHasher::new();
2334        let mut hasher2 = DefaultHasher::new();
2335        value1.hash(&mut hasher1);
2336        value2.hash(&mut hasher2);
2337
2338        assert_eq!(hasher1.finish(), hasher2.finish());
2339    }
2340
2341    #[test]
2342    fn test_abstract_value_top_creates_unknown() {
2343        // CAP-AI-04: top() creates unknown value
2344        let top = AbstractValue::top();
2345
2346        assert_eq!(top.type_, None);
2347        assert_eq!(top.range_, None);
2348        assert_eq!(top.nullable, Nullability::Maybe);
2349        assert!(top.constant.is_none());
2350    }
2351
2352    #[test]
2353    fn test_abstract_value_bottom_creates_contradiction() {
2354        // CAP-AI-04: bottom() creates contradiction
2355        let bottom = AbstractValue::bottom();
2356
2357        assert_eq!(bottom.type_, Some("<bottom>".to_string()));
2358        assert_eq!(bottom.range_, Some((None, None)));
2359        assert_eq!(bottom.nullable, Nullability::Never);
2360        assert!(bottom.constant.is_none());
2361    }
2362
2363    #[test]
2364    fn test_abstract_value_from_constant_int() {
2365        // CAP-AI-03: from_constant for positive int
2366        let value = AbstractValue::from_constant(ConstantValue::Int(5));
2367
2368        assert_eq!(value.type_, Some("int".to_string()));
2369        assert_eq!(value.range_, Some((Some(5), Some(5))));
2370        assert_eq!(value.nullable, Nullability::Never);
2371        assert_eq!(value.constant, Some(ConstantValue::Int(5)));
2372    }
2373
2374    #[test]
2375    fn test_abstract_value_from_constant_negative_int() {
2376        // CAP-AI-03: from_constant for negative int
2377        let value = AbstractValue::from_constant(ConstantValue::Int(-42));
2378
2379        assert_eq!(value.type_, Some("int".to_string()));
2380        assert_eq!(value.range_, Some((Some(-42), Some(-42))));
2381        assert_eq!(value.nullable, Nullability::Never);
2382        assert_eq!(value.constant, Some(ConstantValue::Int(-42)));
2383    }
2384
2385    #[test]
2386    fn test_abstract_value_from_constant_string() {
2387        // CAP-AI-03: from_constant for string
2388        let value = AbstractValue::from_constant(ConstantValue::String("hello".to_string()));
2389
2390        assert_eq!(value.type_, Some("str".to_string()));
2391        assert_eq!(value.nullable, Nullability::Never);
2392        assert!(value.constant.is_some());
2393    }
2394
2395    #[test]
2396    fn test_abstract_value_string_tracks_length() {
2397        // CAP-AI-18: String tracks length in range
2398        let value = AbstractValue::from_constant(ConstantValue::String("hello".to_string()));
2399
2400        // "hello" has length 5
2401        assert_eq!(value.range_, Some((Some(5), Some(5))));
2402    }
2403
2404    #[test]
2405    fn test_abstract_value_from_constant_none() {
2406        // CAP-AI-03: from_constant for Null
2407        let value = AbstractValue::from_constant(ConstantValue::Null);
2408
2409        assert_eq!(value.type_, Some("NoneType".to_string()));
2410        assert_eq!(value.range_, None);
2411        assert_eq!(value.nullable, Nullability::Always);
2412        assert!(value.constant.is_none()); // Null is represented by nullable=Always
2413    }
2414
2415    #[test]
2416    fn test_abstract_value_from_constant_bool() {
2417        // CAP-AI-03: from_constant for bool
2418        let value_true = AbstractValue::from_constant(ConstantValue::Bool(true));
2419        let value_false = AbstractValue::from_constant(ConstantValue::Bool(false));
2420
2421        assert_eq!(value_true.type_, Some("bool".to_string()));
2422        assert_eq!(value_true.range_, Some((Some(1), Some(1)))); // true as 1
2423        assert_eq!(value_false.range_, Some((Some(0), Some(0)))); // false as 0
2424    }
2425
2426    #[test]
2427    fn test_abstract_value_from_constant_float() {
2428        // CAP-AI-03: from_constant for float
2429        let value = AbstractValue::from_constant(ConstantValue::Float(PI));
2430
2431        assert_eq!(value.type_, Some("float".to_string()));
2432        assert_eq!(value.range_, None); // Float ranges not tracked
2433        assert_eq!(value.nullable, Nullability::Never);
2434    }
2435
2436    // =========================================================================
2437    // may_be_zero Tests (CAP-AI-05)
2438    // =========================================================================
2439
2440    #[test]
2441    fn test_may_be_zero_returns_true_when_range_includes_zero() {
2442        // CAP-AI-05: Range includes zero
2443        let value = AbstractValue {
2444            type_: Some("int".to_string()),
2445            range_: Some((Some(-5), Some(5))),
2446            nullable: Nullability::Never,
2447            constant: None,
2448        };
2449        assert!(value.may_be_zero());
2450
2451        // Exact zero
2452        let exact_zero = AbstractValue::from_constant(ConstantValue::Int(0));
2453        assert!(exact_zero.may_be_zero());
2454    }
2455
2456    #[test]
2457    fn test_may_be_zero_returns_false_when_range_excludes_zero() {
2458        // CAP-AI-05: Range excludes zero
2459        let positive = AbstractValue {
2460            type_: Some("int".to_string()),
2461            range_: Some((Some(1), Some(10))),
2462            nullable: Nullability::Never,
2463            constant: None,
2464        };
2465        assert!(!positive.may_be_zero());
2466
2467        let negative = AbstractValue {
2468            type_: Some("int".to_string()),
2469            range_: Some((Some(-10), Some(-1))),
2470            nullable: Nullability::Never,
2471            constant: None,
2472        };
2473        assert!(!negative.may_be_zero());
2474    }
2475
2476    #[test]
2477    fn test_may_be_zero_returns_true_for_unknown_range() {
2478        // CAP-AI-05: Unknown range -> conservative true
2479        let top = AbstractValue::top();
2480        assert!(top.may_be_zero());
2481    }
2482
2483    // =========================================================================
2484    // may_be_null Tests (CAP-AI-06)
2485    // =========================================================================
2486
2487    #[test]
2488    fn test_may_be_null_for_maybe() {
2489        // CAP-AI-06: Maybe nullable -> true
2490        let value = AbstractValue {
2491            type_: None,
2492            range_: None,
2493            nullable: Nullability::Maybe,
2494            constant: None,
2495        };
2496        assert!(value.may_be_null());
2497    }
2498
2499    #[test]
2500    fn test_may_be_null_for_never() {
2501        // CAP-AI-06: Never nullable -> false
2502        let value = AbstractValue::from_constant(ConstantValue::Int(5));
2503        assert!(!value.may_be_null());
2504    }
2505
2506    #[test]
2507    fn test_may_be_null_for_always() {
2508        // CAP-AI-06: Always nullable -> true
2509        let value = AbstractValue::from_constant(ConstantValue::Null);
2510        assert!(value.may_be_null());
2511    }
2512
2513    // =========================================================================
2514    // is_constant Tests
2515    // =========================================================================
2516
2517    #[test]
2518    fn test_is_constant_true_when_constant_set() {
2519        let value = AbstractValue::from_constant(ConstantValue::Int(42));
2520        assert!(value.is_constant());
2521    }
2522
2523    #[test]
2524    fn test_is_constant_false_when_constant_none() {
2525        let value = AbstractValue::top();
2526        assert!(!value.is_constant());
2527    }
2528
2529    // =========================================================================
2530    // AbstractState Tests (CAP-AI-07)
2531    // =========================================================================
2532
2533    #[test]
2534    fn test_abstract_state_empty_initialization() {
2535        let state = AbstractState::new();
2536        assert!(state.values.is_empty());
2537    }
2538
2539    #[test]
2540    fn test_abstract_state_get_returns_value_for_existing_var() {
2541        let mut state = AbstractState::new();
2542        let value = AbstractValue::from_constant(ConstantValue::Int(5));
2543        state.values.insert("x".to_string(), value.clone());
2544
2545        let retrieved = state.get("x");
2546        assert_eq!(retrieved.range_, Some((Some(5), Some(5))));
2547    }
2548
2549    #[test]
2550    fn test_abstract_state_get_returns_top_for_missing_var() {
2551        // CAP-AI-07: Missing vars default to top
2552        let state = AbstractState::new();
2553        let value = state.get("nonexistent");
2554
2555        assert_eq!(value.type_, None);
2556        assert_eq!(value.range_, None);
2557        assert_eq!(value.nullable, Nullability::Maybe);
2558    }
2559
2560    #[test]
2561    fn test_abstract_state_set_returns_new_state() {
2562        // Immutable update pattern
2563        let state1 = AbstractState::new();
2564        let state2 = state1.set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
2565
2566        // Original unchanged
2567        assert!(state1.values.is_empty());
2568        // New state has the value
2569        assert!(state2.values.contains_key("x"));
2570    }
2571
2572    #[test]
2573    fn test_abstract_state_copy_creates_independent_copy() {
2574        let mut state1 = AbstractState::new();
2575        state1.values.insert(
2576            "x".to_string(),
2577            AbstractValue::from_constant(ConstantValue::Int(5)),
2578        );
2579
2580        let state2 = state1.copy();
2581
2582        // Modify original
2583        state1.values.insert(
2584            "y".to_string(),
2585            AbstractValue::from_constant(ConstantValue::Int(10)),
2586        );
2587
2588        // Copy should not have y
2589        assert!(state2.values.contains_key("x"));
2590        assert!(!state2.values.contains_key("y"));
2591    }
2592
2593    #[test]
2594    fn test_abstract_state_equality() {
2595        let state1 =
2596            AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
2597        let state2 =
2598            AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
2599        let state3 =
2600            AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(10)));
2601
2602        assert_eq!(state1, state2);
2603        assert_ne!(state1, state3);
2604    }
2605
2606    // =========================================================================
2607    // AbstractInterpInfo Tests (CAP-AI-21, CAP-AI-22)
2608    // =========================================================================
2609
2610    #[test]
2611    fn test_abstract_interp_info_has_required_fields() {
2612        let info = AbstractInterpInfo::new("test_func");
2613
2614        assert_eq!(info.function_name, "test_func");
2615        assert!(info.state_in.is_empty());
2616        assert!(info.state_out.is_empty());
2617        assert!(info.potential_div_zero.is_empty());
2618        assert!(info.potential_null_deref.is_empty());
2619    }
2620
2621    #[test]
2622    fn test_value_at_returns_abstract_value_at_block_entry() {
2623        let mut info = AbstractInterpInfo::new("test");
2624        let state =
2625            AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(42)));
2626        info.state_in.insert(0, state);
2627
2628        let value = info.value_at(0, "x");
2629        assert_eq!(value.range_, Some((Some(42), Some(42))));
2630    }
2631
2632    #[test]
2633    fn test_value_at_returns_top_for_missing_block() {
2634        let info = AbstractInterpInfo::new("test");
2635        let value = info.value_at(999, "x");
2636
2637        // Should return top() for missing block
2638        assert_eq!(value.type_, None);
2639        assert_eq!(value.range_, None);
2640    }
2641
2642    #[test]
2643    fn test_value_at_exit_returns_value_at_block_exit() {
2644        let mut info = AbstractInterpInfo::new("test");
2645        let state =
2646            AbstractState::new().set("y", AbstractValue::from_constant(ConstantValue::Int(100)));
2647        info.state_out.insert(1, state);
2648
2649        let value = info.value_at_exit(1, "y");
2650        assert_eq!(value.range_, Some((Some(100), Some(100))));
2651    }
2652
2653    #[test]
2654    fn test_range_at_returns_range_tuple() {
2655        let mut info = AbstractInterpInfo::new("test");
2656        let state =
2657            AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
2658        info.state_in.insert(0, state);
2659
2660        let range = info.range_at(0, "x");
2661        assert_eq!(range, Some((Some(5), Some(5))));
2662    }
2663
2664    #[test]
2665    fn test_type_at_returns_inferred_type() {
2666        let mut info = AbstractInterpInfo::new("test");
2667        let state = AbstractState::new().set(
2668            "x",
2669            AbstractValue::from_constant(ConstantValue::String("hello".to_string())),
2670        );
2671        info.state_in.insert(0, state);
2672
2673        let type_ = info.type_at(0, "x");
2674        assert_eq!(type_, Some("str".to_string()));
2675    }
2676
2677    #[test]
2678    fn test_is_definitely_not_null_for_never_nullable() {
2679        let mut info = AbstractInterpInfo::new("test");
2680        let state =
2681            AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
2682        info.state_in.insert(0, state);
2683
2684        assert!(info.is_definitely_not_null(0, "x"));
2685    }
2686
2687    #[test]
2688    fn test_is_definitely_not_null_for_maybe_nullable() {
2689        let mut info = AbstractInterpInfo::new("test");
2690        let state = AbstractState::new().set("x", AbstractValue::top());
2691        info.state_in.insert(0, state);
2692
2693        assert!(!info.is_definitely_not_null(0, "x"));
2694    }
2695
2696    #[test]
2697    fn test_get_constants_returns_known_constant_values() {
2698        let mut info = AbstractInterpInfo::new("test");
2699        let state = AbstractState::new()
2700            .set("x", AbstractValue::from_constant(ConstantValue::Int(5)))
2701            .set(
2702                "y",
2703                AbstractValue::from_constant(ConstantValue::String("hello".to_string())),
2704            )
2705            .set("z", AbstractValue::top()); // Not a constant
2706        info.state_out.insert(0, state);
2707
2708        let constants = info.get_constants();
2709        assert_eq!(constants.len(), 2);
2710        assert!(constants.contains_key("x"));
2711        assert!(constants.contains_key("y"));
2712        assert!(!constants.contains_key("z"));
2713    }
2714
2715    #[test]
2716    fn test_abstract_interp_to_json_serializable() {
2717        let mut info = AbstractInterpInfo::new("example");
2718        let state =
2719            AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(42)));
2720        info.state_in.insert(0, state.clone());
2721        info.state_out.insert(0, state);
2722        info.potential_div_zero.push((10, "y".to_string()));
2723        info.potential_null_deref.push((15, "obj".to_string()));
2724
2725        let json = info.to_json();
2726
2727        // Verify it's valid JSON
2728        assert!(json.is_object());
2729        assert_eq!(json["function"], "example");
2730        assert!(json["state_in"].is_object());
2731        assert!(json["state_out"].is_object());
2732        assert!(json["potential_div_zero"].is_array());
2733        assert!(json["potential_null_deref"].is_array());
2734
2735        // Verify serialization works
2736        let serialized = serde_json::to_string(&json);
2737        assert!(serialized.is_ok());
2738    }
2739
2740    // =========================================================================
2741    // Phase 8: Multi-Language Keyword Tests (CAP-AI-15, CAP-AI-16, CAP-AI-17)
2742    // =========================================================================
2743
2744    #[test]
2745    fn test_python_none_keyword_recognized() {
2746        // CAP-AI-15: Python None keyword
2747        let keywords = get_null_keywords("python");
2748        assert!(keywords.contains(&"None"));
2749    }
2750
2751    #[test]
2752    fn test_typescript_null_keyword_recognized() {
2753        // CAP-AI-15: TypeScript null keyword
2754        let keywords = get_null_keywords("typescript");
2755        assert!(keywords.contains(&"null"));
2756    }
2757
2758    #[test]
2759    fn test_typescript_undefined_keyword_recognized() {
2760        // CAP-AI-15: TypeScript undefined keyword
2761        let keywords = get_null_keywords("typescript");
2762        assert!(keywords.contains(&"undefined"));
2763    }
2764
2765    #[test]
2766    fn test_go_nil_keyword_recognized() {
2767        // CAP-AI-15: Go nil keyword
2768        let keywords = get_null_keywords("go");
2769        assert!(keywords.contains(&"nil"));
2770    }
2771
2772    #[test]
2773    fn test_rust_has_no_null_keyword() {
2774        // CAP-AI-15: Rust has no null (None is Option::None, not null)
2775        let keywords = get_null_keywords("rust");
2776        assert!(keywords.is_empty(), "Rust should have no null keywords");
2777    }
2778
2779    #[test]
2780    fn test_python_boolean_capitalized() {
2781        // CAP-AI-16: Python uses True/False (capitalized)
2782        let bools = get_boolean_keywords("python");
2783        assert_eq!(bools.get("True"), Some(&true));
2784        assert_eq!(bools.get("False"), Some(&false));
2785    }
2786
2787    #[test]
2788    fn test_typescript_boolean_lowercase() {
2789        // CAP-AI-16: TypeScript uses true/false (lowercase)
2790        let bools = get_boolean_keywords("typescript");
2791        assert_eq!(bools.get("true"), Some(&true));
2792        assert_eq!(bools.get("false"), Some(&false));
2793    }
2794
2795    #[test]
2796    fn test_python_comment_pattern() {
2797        // CAP-AI-17: Python uses # for comments
2798        let pattern = get_comment_pattern("python");
2799        assert_eq!(pattern, "#");
2800    }
2801
2802    #[test]
2803    fn test_typescript_comment_pattern() {
2804        // CAP-AI-17: TypeScript uses // for comments
2805        let pattern = get_comment_pattern("typescript");
2806        assert_eq!(pattern, "//");
2807    }
2808
2809    // =========================================================================
2810    // Arithmetic Tests (CAP-AI-13) - Phase 7
2811    // =========================================================================
2812
2813    #[test]
2814    fn test_arithmetic_add() {
2815        // CAP-AI-13: Abstract arithmetic - addition
2816        // [5, 5] + 3 -> [8, 8]
2817        let operand = AbstractValue::from_constant(ConstantValue::Int(5));
2818        let result = apply_arithmetic(&operand, '+', 3);
2819
2820        assert_eq!(result.range_, Some((Some(8), Some(8))));
2821        assert_eq!(result.constant, Some(ConstantValue::Int(8)));
2822    }
2823
2824    #[test]
2825    fn test_arithmetic_subtract() {
2826        // CAP-AI-13: Abstract arithmetic - subtraction
2827        // [10, 10] - 3 -> [7, 7]
2828        let operand = AbstractValue::from_constant(ConstantValue::Int(10));
2829        let result = apply_arithmetic(&operand, '-', 3);
2830
2831        assert_eq!(result.range_, Some((Some(7), Some(7))));
2832        assert_eq!(result.constant, Some(ConstantValue::Int(7)));
2833    }
2834
2835    #[test]
2836    fn test_arithmetic_multiply() {
2837        // CAP-AI-13: Abstract arithmetic - multiplication
2838        // [4, 4] * 2 -> [8, 8]
2839        let operand = AbstractValue::from_constant(ConstantValue::Int(4));
2840        let result = apply_arithmetic(&operand, '*', 2);
2841
2842        assert_eq!(result.range_, Some((Some(8), Some(8))));
2843        assert_eq!(result.constant, Some(ConstantValue::Int(8)));
2844    }
2845
2846    #[test]
2847    fn test_arithmetic_on_range() {
2848        // CAP-AI-13: Arithmetic on a range
2849        // [1, 5] + 10 -> [11, 15]
2850        let operand = AbstractValue {
2851            type_: Some("int".to_string()),
2852            range_: Some((Some(1), Some(5))),
2853            nullable: Nullability::Never,
2854            constant: None,
2855        };
2856
2857        let result = apply_arithmetic(&operand, '+', 10);
2858
2859        assert_eq!(result.range_, Some((Some(11), Some(15))));
2860        // Not a constant because range is not a single value
2861        assert!(result.constant.is_none());
2862    }
2863
2864    #[test]
2865    fn test_arithmetic_overflow_saturates_add() {
2866        // TIGER-PASS1-11: Overflow should widen to unbounded (None)
2867        // i64::MAX + 1 -> widened to unbounded
2868        let operand = AbstractValue::from_constant(ConstantValue::Int(i64::MAX));
2869        let result = apply_arithmetic(&operand, '+', 1);
2870
2871        // Range should contain at least one None (widened due to overflow)
2872        match result.range_ {
2873            Some((low, high)) => {
2874                // Either low or high should be None due to saturation
2875                assert!(
2876                    low.is_none() || high.is_none(),
2877                    "Overflow should widen to unbounded: got ({:?}, {:?})",
2878                    low,
2879                    high
2880                );
2881            }
2882            None => {
2883                // No range at all is also acceptable
2884            }
2885        }
2886    }
2887
2888    #[test]
2889    fn test_arithmetic_overflow_saturates_sub() {
2890        // TIGER-PASS1-11: Underflow should widen to unbounded
2891        // i64::MIN - 1 -> widened to unbounded
2892        let operand = AbstractValue::from_constant(ConstantValue::Int(i64::MIN));
2893        let result = apply_arithmetic(&operand, '-', 1);
2894
2895        // Range should contain at least one None (widened due to overflow)
2896        if let Some((low, high)) = result.range_ {
2897            assert!(
2898                low.is_none() || high.is_none(),
2899                "Underflow should widen to unbounded: got ({:?}, {:?})",
2900                low,
2901                high
2902            );
2903        }
2904    }
2905
2906    #[test]
2907    fn test_arithmetic_multiply_by_negative() {
2908        // Multiplication by negative swaps bounds
2909        // [2, 4] * (-3) -> [-12, -6]
2910        let operand = AbstractValue {
2911            type_: Some("int".to_string()),
2912            range_: Some((Some(2), Some(4))),
2913            nullable: Nullability::Never,
2914            constant: None,
2915        };
2916
2917        let result = apply_arithmetic(&operand, '*', -3);
2918
2919        assert_eq!(result.range_, Some((Some(-12), Some(-6))));
2920    }
2921
2922    #[test]
2923    fn test_arithmetic_multiply_by_zero() {
2924        // Multiplication by zero gives [0, 0]
2925        let operand = AbstractValue {
2926            type_: Some("int".to_string()),
2927            range_: Some((Some(1), Some(100))),
2928            nullable: Nullability::Never,
2929            constant: None,
2930        };
2931
2932        let result = apply_arithmetic(&operand, '*', 0);
2933
2934        assert_eq!(result.range_, Some((Some(0), Some(0))));
2935    }
2936
2937    #[test]
2938    fn test_arithmetic_preserves_type() {
2939        // Arithmetic should preserve the type
2940        let operand = AbstractValue::from_constant(ConstantValue::Int(5));
2941        let result = apply_arithmetic(&operand, '+', 3);
2942
2943        assert_eq!(result.type_, Some("int".to_string()));
2944    }
2945
2946    #[test]
2947    fn test_arithmetic_preserves_nullable() {
2948        // Arithmetic should preserve nullability
2949        let operand = AbstractValue::from_constant(ConstantValue::Int(5));
2950        assert_eq!(operand.nullable, Nullability::Never);
2951
2952        let result = apply_arithmetic(&operand, '+', 3);
2953        assert_eq!(result.nullable, Nullability::Never);
2954    }
2955
2956    #[test]
2957    fn test_arithmetic_unknown_op() {
2958        // Unknown operator should widen to unbounded
2959        let operand = AbstractValue::from_constant(ConstantValue::Int(5));
2960        let result = apply_arithmetic(&operand, '^', 3); // ^ not supported
2961
2962        // Should widen to unbounded
2963        assert_eq!(result.range_, Some((None, None)));
2964        assert!(result.constant.is_none());
2965    }
2966
2967    #[test]
2968    fn test_arithmetic_on_no_range() {
2969        // Arithmetic on value with no range returns no range
2970        let operand = AbstractValue::top();
2971        let result = apply_arithmetic(&operand, '+', 5);
2972
2973        assert!(result.range_.is_none());
2974    }
2975
2976    // =========================================================================
2977    // Phase 6 Tests: Join and Widening (CAP-AI-08, CAP-AI-09)
2978    // =========================================================================
2979
2980    #[test]
2981    fn test_join_values_ranges_union() {
2982        // CAP-AI-08: Join takes union of ranges [1,1] join [10,10] -> [1,10]
2983        let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
2984        let val2 = AbstractValue::from_constant(ConstantValue::Int(10));
2985
2986        let joined = join_values(&val1, &val2);
2987
2988        // Range should be union: [1, 10]
2989        assert_eq!(joined.range_, Some((Some(1), Some(10))));
2990    }
2991
2992    #[test]
2993    fn test_join_values_loses_constant_on_disagreement() {
2994        // CAP-AI-08: Constant lost when values disagree
2995        let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
2996        let val2 = AbstractValue::from_constant(ConstantValue::Int(10));
2997
2998        let joined = join_values(&val1, &val2);
2999
3000        assert!(
3001            joined.constant.is_none(),
3002            "Constant should be lost on disagreement"
3003        );
3004    }
3005
3006    #[test]
3007    fn test_join_values_preserves_constant_on_agreement() {
3008        // CAP-AI-08: Constant kept when values agree
3009        let val1 = AbstractValue::from_constant(ConstantValue::Int(5));
3010        let val2 = AbstractValue::from_constant(ConstantValue::Int(5));
3011
3012        let joined = join_values(&val1, &val2);
3013
3014        assert_eq!(joined.constant, Some(ConstantValue::Int(5)));
3015    }
3016
3017    #[test]
3018    fn test_join_values_nullable_maybe_if_any_maybe() {
3019        // CAP-AI-08: Nullable becomes MAYBE if either is MAYBE
3020        let val1 = AbstractValue {
3021            type_: None,
3022            range_: None,
3023            nullable: Nullability::Never,
3024            constant: None,
3025        };
3026        let val2 = AbstractValue {
3027            type_: None,
3028            range_: None,
3029            nullable: Nullability::Maybe,
3030            constant: None,
3031        };
3032
3033        let joined = join_values(&val1, &val2);
3034
3035        assert_eq!(joined.nullable, Nullability::Maybe);
3036    }
3037
3038    #[test]
3039    fn test_join_values_nullable_never_if_both_never() {
3040        // CAP-AI-08: NEVER + NEVER = NEVER
3041        let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
3042        let val2 = AbstractValue::from_constant(ConstantValue::Int(2));
3043
3044        let joined = join_values(&val1, &val2);
3045
3046        assert_eq!(joined.nullable, Nullability::Never);
3047    }
3048
3049    #[test]
3050    fn test_join_values_type_preserved_when_same() {
3051        // CAP-AI-08: Type preserved when both values have same type
3052        let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
3053        let val2 = AbstractValue::from_constant(ConstantValue::Int(2));
3054
3055        let joined = join_values(&val1, &val2);
3056
3057        assert_eq!(joined.type_, Some("int".to_string()));
3058    }
3059
3060    #[test]
3061    fn test_join_values_type_lost_when_different() {
3062        // CAP-AI-08: Type lost when values have different types
3063        let val1 = AbstractValue::from_constant(ConstantValue::Int(1));
3064        let val2 = AbstractValue::from_constant(ConstantValue::String("hello".to_string()));
3065
3066        let joined = join_values(&val1, &val2);
3067
3068        assert_eq!(joined.type_, None);
3069    }
3070
3071    #[test]
3072    fn test_join_states_empty() {
3073        // CAP-AI-08: Join of empty states is empty
3074        let states: Vec<&AbstractState> = vec![];
3075        let joined = join_states(&states);
3076
3077        assert!(joined.values.is_empty());
3078    }
3079
3080    #[test]
3081    fn test_join_states_single() {
3082        // CAP-AI-08: Join of single state returns that state
3083        let state =
3084            AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(5)));
3085        let states: Vec<&AbstractState> = vec![&state];
3086
3087        let joined = join_states(&states);
3088
3089        assert_eq!(joined.get("x").range_, Some((Some(5), Some(5))));
3090    }
3091
3092    #[test]
3093    fn test_join_states_multiple() {
3094        // CAP-AI-08: Join of multiple states combines variables
3095        let state1 =
3096            AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(1)));
3097        let state2 =
3098            AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(10)));
3099        let states: Vec<&AbstractState> = vec![&state1, &state2];
3100
3101        let joined = join_states(&states);
3102
3103        // x should have range [1, 10]
3104        assert_eq!(joined.get("x").range_, Some((Some(1), Some(10))));
3105    }
3106
3107    #[test]
3108    fn test_widen_value_upper_bound_to_infinity() {
3109        // CAP-AI-09: Growing upper bound -> widen to +inf (None)
3110        let old = AbstractValue {
3111            type_: Some("int".to_string()),
3112            range_: Some((Some(0), Some(5))),
3113            nullable: Nullability::Never,
3114            constant: None,
3115        };
3116        let new = AbstractValue {
3117            type_: Some("int".to_string()),
3118            range_: Some((Some(0), Some(10))), // Upper bound grew
3119            nullable: Nullability::Never,
3120            constant: None,
3121        };
3122
3123        let widened = widen_value(&old, &new);
3124
3125        // Upper bound should be widened to +inf (None)
3126        assert_eq!(widened.range_, Some((Some(0), None)));
3127    }
3128
3129    #[test]
3130    fn test_widen_value_lower_bound_to_infinity() {
3131        // CAP-AI-09: Growing lower bound -> widen to -inf (None)
3132        let old = AbstractValue {
3133            type_: Some("int".to_string()),
3134            range_: Some((Some(-5), Some(10))),
3135            nullable: Nullability::Never,
3136            constant: None,
3137        };
3138        let new = AbstractValue {
3139            type_: Some("int".to_string()),
3140            range_: Some((Some(-10), Some(10))), // Lower bound grew (more negative)
3141            nullable: Nullability::Never,
3142            constant: None,
3143        };
3144
3145        let widened = widen_value(&old, &new);
3146
3147        // Lower bound should be widened to -inf (None)
3148        assert_eq!(widened.range_, Some((None, Some(10))));
3149    }
3150
3151    #[test]
3152    fn test_widen_value_loses_constant() {
3153        // CAP-AI-09: Widening loses constant information
3154        let old = AbstractValue::from_constant(ConstantValue::Int(5));
3155        let new = AbstractValue::from_constant(ConstantValue::Int(6));
3156
3157        let widened = widen_value(&old, &new);
3158
3159        assert!(widened.constant.is_none(), "Widening should lose constant");
3160    }
3161
3162    #[test]
3163    fn test_widen_value_stable_bounds_not_widened() {
3164        // CAP-AI-09: Stable or shrinking bounds are not widened
3165        let old = AbstractValue {
3166            type_: Some("int".to_string()),
3167            range_: Some((Some(0), Some(10))),
3168            nullable: Nullability::Never,
3169            constant: None,
3170        };
3171        let new = AbstractValue {
3172            type_: Some("int".to_string()),
3173            range_: Some((Some(0), Some(10))), // Same bounds
3174            nullable: Nullability::Never,
3175            constant: None,
3176        };
3177
3178        let widened = widen_value(&old, &new);
3179
3180        // Bounds should remain the same
3181        assert_eq!(widened.range_, Some((Some(0), Some(10))));
3182    }
3183
3184    #[test]
3185    fn test_widen_state_applies_to_all_vars() {
3186        // CAP-AI-09: Widening applies to all variables in both states
3187        let old = AbstractState::new()
3188            .set("x", AbstractValue::from_constant(ConstantValue::Int(5)))
3189            .set("y", AbstractValue::from_constant(ConstantValue::Int(0)));
3190        let new = AbstractState::new()
3191            .set("x", AbstractValue::from_constant(ConstantValue::Int(10)))
3192            .set("y", AbstractValue::from_constant(ConstantValue::Int(0)));
3193
3194        let widened = widen_state(&old, &new);
3195
3196        // x: old=[5,5], new=[10,10]
3197        // Lower bound went from 5 to 10 (grew upward, not downward) -> keep new value (10)
3198        // Upper bound went from 5 to 10 (grew upward) -> widen to +inf (None)
3199        // Result: [10, None]
3200        assert_eq!(widened.get("x").range_, Some((Some(10), None)));
3201        // y should be unchanged (same bounds)
3202        assert_eq!(widened.get("y").range_, Some((Some(0), Some(0))));
3203    }
3204
3205    // =========================================================================
3206    // Phase 9 Tests: RHS Parsing (CAP-AI-14)
3207    // =========================================================================
3208
3209    #[test]
3210    fn test_extract_rhs_simple_assignment() {
3211        // CAP-AI-14: Extract RHS from simple assignment
3212        let rhs = extract_rhs("x = a + b", "x");
3213        assert_eq!(rhs, Some("a + b".to_string()));
3214
3215        let rhs = extract_rhs("foo = 42", "foo");
3216        assert_eq!(rhs, Some("42".to_string()));
3217
3218        let rhs = extract_rhs("result = None", "result");
3219        assert_eq!(rhs, Some("None".to_string()));
3220    }
3221
3222    #[test]
3223    fn test_extract_rhs_augmented_assignment() {
3224        // TIGER-PASS2-2: Augmented assignments converted to regular form
3225        let rhs = extract_rhs("x += 5", "x");
3226        assert_eq!(rhs, Some("x + 5".to_string()));
3227
3228        let rhs = extract_rhs("y -= 3", "y");
3229        assert_eq!(rhs, Some("y - 3".to_string()));
3230
3231        let rhs = extract_rhs("count *= 2", "count");
3232        assert_eq!(rhs, Some("count * 2".to_string()));
3233    }
3234
3235    #[test]
3236    fn test_extract_rhs_with_spaces() {
3237        // Various spacing patterns
3238        let rhs = extract_rhs("x=5", "x");
3239        assert_eq!(rhs, Some("5".to_string()));
3240
3241        let rhs = extract_rhs("x =5", "x");
3242        assert_eq!(rhs, Some("5".to_string()));
3243
3244        let rhs = extract_rhs("x= 5", "x");
3245        assert_eq!(rhs, Some("5".to_string()));
3246    }
3247
3248    #[test]
3249    fn test_extract_rhs_not_found() {
3250        // No assignment to this variable
3251        let rhs = extract_rhs("y = 5", "x");
3252        assert_eq!(rhs, None);
3253
3254        // Partial match should not match
3255        let rhs = extract_rhs("xy = 5", "x");
3256        assert_eq!(rhs, None);
3257    }
3258
3259    #[test]
3260    fn test_strip_comment_python() {
3261        // TIGER-PASS1-14: Single-line comments stripped
3262        let stripped = strip_comment("x = 5  # this is a comment", "python");
3263        assert_eq!(stripped, "x = 5  ");
3264
3265        let stripped = strip_comment("x = 5", "python");
3266        assert_eq!(stripped, "x = 5");
3267    }
3268
3269    #[test]
3270    fn test_strip_comment_typescript() {
3271        let stripped = strip_comment("x = 5  // this is a comment", "typescript");
3272        assert_eq!(stripped, "x = 5  ");
3273
3274        let stripped = strip_comment("x = 5", "typescript");
3275        assert_eq!(stripped, "x = 5");
3276    }
3277
3278    #[test]
3279    fn test_strip_comment_preserves_string() {
3280        // Comment marker inside string should not be stripped
3281        let stripped = strip_comment("x = \"hello # world\"", "python");
3282        assert_eq!(stripped, "x = \"hello # world\"");
3283
3284        let stripped = strip_comment("x = 'hello // world'", "typescript");
3285        assert_eq!(stripped, "x = 'hello // world'");
3286    }
3287
3288    #[test]
3289    fn test_strip_strings_blanks_path_separators() {
3290        // Path inside string: slashes should be blanked
3291        let result = strip_strings("Path::new(\"src/main.rs\")", "rust");
3292        assert_eq!(result, "Path::new(\"           \")");
3293        assert!(!result.contains('/'), "slashes inside strings must be blanked");
3294    }
3295
3296    #[test]
3297    fn test_strip_strings_preserves_code() {
3298        // Division operator outside strings should be preserved
3299        let result = strip_strings("let ratio = a / b;", "rust");
3300        assert_eq!(result, "let ratio = a / b;");
3301    }
3302
3303    #[test]
3304    fn test_strip_strings_handles_escapes() {
3305        // Escaped quote inside string should not end the string
3306        let result = strip_strings(r#"let s = "path/to/\"file\""; a / b"#, "rust");
3307        assert!(result.contains("a / b"), "code division must survive");
3308        // The path/to part inside the string should be blanked
3309        assert!(!result[8..25].contains('/'), "slashes in string must be blanked");
3310    }
3311
3312    #[test]
3313    fn test_strip_strings_single_quotes() {
3314        let result = strip_strings("let c = '/'; x / y", "rust");
3315        assert!(result.contains("x / y"), "code division must survive");
3316        // The '/' char literal should be blanked
3317        assert_eq!(result.matches('/').count(), 1, "only code division remains");
3318    }
3319
3320    #[test]
3321    fn test_strip_strings_rust_raw_string() {
3322        // r#"..."# raw strings: contents must be blanked
3323        let result = strip_strings(r##"let xml = r#"</coverage>"#;"##, "rust");
3324        assert!(!result.contains('/'), "slashes inside raw strings must be blanked");
3325        assert!(!result.contains("coverage"), "identifiers inside raw strings must be blanked");
3326    }
3327
3328    #[test]
3329    fn test_strip_strings_rust_raw_no_hashes() {
3330        // r"..." raw strings without hashes
3331        let result = strip_strings(r#"let p = r"/src/main.rs"; a / b"#, "rust");
3332        assert!(result.contains("a / b"), "code division must survive");
3333        // Only the code `/` should remain
3334        assert_eq!(result.matches('/').count(), 1, "only code division remains");
3335    }
3336
3337    #[test]
3338    fn test_strip_strings_rust_raw_double_hash() {
3339        // r##"..."## raw strings
3340        let result = strip_strings(r###"let s = r##"a/b"##;"###, "rust");
3341        assert!(!result.contains("a/b"), "contents of r##\"...\"## must be blanked");
3342    }
3343
3344    #[test]
3345    fn test_parse_simple_arithmetic_var_plus_const() {
3346        // CAP-AI-13: Variable + constant
3347        let result = parse_simple_arithmetic("a + 1");
3348        assert_eq!(result, Some(("a".to_string(), '+', 1)));
3349
3350        let result = parse_simple_arithmetic("count - 5");
3351        assert_eq!(result, Some(("count".to_string(), '-', 5)));
3352
3353        let result = parse_simple_arithmetic("x * 2");
3354        assert_eq!(result, Some(("x".to_string(), '*', 2)));
3355    }
3356
3357    #[test]
3358    fn test_parse_simple_arithmetic_const_plus_var() {
3359        // Commutative: const + var (only for + and *)
3360        let result = parse_simple_arithmetic("1 + a");
3361        assert_eq!(result, Some(("a".to_string(), '+', 1)));
3362
3363        let result = parse_simple_arithmetic("2 * x");
3364        assert_eq!(result, Some(("x".to_string(), '*', 2)));
3365    }
3366
3367    #[test]
3368    fn test_parse_simple_arithmetic_negative_const() {
3369        // Negative constants
3370        let result = parse_simple_arithmetic("a + -5");
3371        assert_eq!(result, Some(("a".to_string(), '+', -5)));
3372    }
3373
3374    #[test]
3375    fn test_parse_simple_arithmetic_no_match() {
3376        // Complex expressions don't match
3377        let result = parse_simple_arithmetic("a + b"); // Two variables
3378        assert_eq!(result, None);
3379
3380        let result = parse_simple_arithmetic("5"); // Just a constant
3381        assert_eq!(result, None);
3382
3383        let result = parse_simple_arithmetic("foo"); // Just a variable
3384        assert_eq!(result, None);
3385    }
3386
3387    #[test]
3388    fn test_is_identifier() {
3389        assert!(is_identifier("x"));
3390        assert!(is_identifier("foo"));
3391        assert!(is_identifier("_bar"));
3392        assert!(is_identifier("var123"));
3393        assert!(is_identifier("__init__"));
3394
3395        assert!(!is_identifier(""));
3396        assert!(!is_identifier("123var"));
3397        assert!(!is_identifier("foo.bar"));
3398        assert!(!is_identifier("foo bar"));
3399        assert!(!is_identifier("foo-bar"));
3400    }
3401
3402    #[test]
3403    fn test_parse_rhs_abstract_integer() {
3404        // CAP-AI-14: Integer literal
3405        let state = AbstractState::new();
3406        let val = parse_rhs_abstract("x = 5", "x", &state, "python");
3407
3408        assert_eq!(val.range_, Some((Some(5), Some(5))));
3409        assert_eq!(val.constant, Some(ConstantValue::Int(5)));
3410        assert_eq!(val.type_, Some("int".to_string()));
3411    }
3412
3413    #[test]
3414    fn test_parse_rhs_abstract_negative_integer() {
3415        let state = AbstractState::new();
3416        let val = parse_rhs_abstract("x = -42", "x", &state, "python");
3417
3418        assert_eq!(val.range_, Some((Some(-42), Some(-42))));
3419        assert_eq!(val.constant, Some(ConstantValue::Int(-42)));
3420    }
3421
3422    #[test]
3423    fn test_parse_rhs_abstract_float() {
3424        // CAP-AI-14: Float literal
3425        let state = AbstractState::new();
3426        let val = parse_rhs_abstract("x = 3.14", "x", &state, "python");
3427
3428        assert_eq!(val.type_, Some("float".to_string()));
3429        if let Some(ConstantValue::Float(f)) = val.constant {
3430            assert_eq!(f, 3.14);
3431        } else {
3432            panic!("Expected float constant");
3433        }
3434    }
3435
3436    #[test]
3437    fn test_parse_rhs_abstract_string_double_quotes() {
3438        // CAP-AI-14: String literal with double quotes
3439        let state = AbstractState::new();
3440        let val = parse_rhs_abstract("x = \"hello\"", "x", &state, "python");
3441
3442        assert_eq!(val.type_, Some("str".to_string()));
3443        assert_eq!(
3444            val.constant,
3445            Some(ConstantValue::String("hello".to_string()))
3446        );
3447        // CAP-AI-18: String length tracked
3448        assert_eq!(val.range_, Some((Some(5), Some(5))));
3449    }
3450
3451    #[test]
3452    fn test_parse_rhs_abstract_string_single_quotes() {
3453        // CAP-AI-14: String literal with single quotes
3454        let state = AbstractState::new();
3455        let val = parse_rhs_abstract("x = 'world'", "x", &state, "python");
3456
3457        assert_eq!(val.type_, Some("str".to_string()));
3458        assert_eq!(
3459            val.constant,
3460            Some(ConstantValue::String("world".to_string()))
3461        );
3462    }
3463
3464    #[test]
3465    fn test_parse_rhs_abstract_python_none() {
3466        // CAP-AI-15: Python None
3467        let state = AbstractState::new();
3468        let val = parse_rhs_abstract("x = None", "x", &state, "python");
3469
3470        assert_eq!(val.nullable, Nullability::Always);
3471        assert_eq!(val.type_, Some("NoneType".to_string()));
3472    }
3473
3474    #[test]
3475    fn test_parse_rhs_abstract_typescript_null() {
3476        // CAP-AI-15: TypeScript null
3477        let state = AbstractState::new();
3478        let val = parse_rhs_abstract("x = null", "x", &state, "typescript");
3479
3480        assert_eq!(val.nullable, Nullability::Always);
3481    }
3482
3483    #[test]
3484    fn test_parse_rhs_abstract_typescript_undefined() {
3485        // TIGER-PASS2-8: TypeScript undefined tracked separately
3486        let state = AbstractState::new();
3487        let val = parse_rhs_abstract("x = undefined", "x", &state, "typescript");
3488
3489        assert_eq!(val.nullable, Nullability::Always);
3490        assert_eq!(val.type_, Some("undefined".to_string()));
3491    }
3492
3493    #[test]
3494    fn test_parse_rhs_abstract_go_nil() {
3495        // CAP-AI-15: Go nil
3496        let state = AbstractState::new();
3497        let val = parse_rhs_abstract("x = nil", "x", &state, "go");
3498
3499        assert_eq!(val.nullable, Nullability::Always);
3500    }
3501
3502    #[test]
3503    fn test_parse_rhs_abstract_python_bool() {
3504        // CAP-AI-16: Python True/False (capitalized)
3505        let state = AbstractState::new();
3506        let val = parse_rhs_abstract("x = True", "x", &state, "python");
3507
3508        assert_eq!(val.type_, Some("bool".to_string()));
3509        assert_eq!(val.constant, Some(ConstantValue::Bool(true)));
3510
3511        let val = parse_rhs_abstract("y = False", "y", &state, "python");
3512        assert_eq!(val.constant, Some(ConstantValue::Bool(false)));
3513    }
3514
3515    #[test]
3516    fn test_parse_rhs_abstract_typescript_bool() {
3517        // CAP-AI-16: TypeScript true/false (lowercase)
3518        let state = AbstractState::new();
3519        let val = parse_rhs_abstract("x = true", "x", &state, "typescript");
3520
3521        assert_eq!(val.type_, Some("bool".to_string()));
3522        assert_eq!(val.constant, Some(ConstantValue::Bool(true)));
3523    }
3524
3525    #[test]
3526    fn test_parse_rhs_abstract_variable_copy() {
3527        // CAP-AI-19: Variable copy (y = x copies value)
3528        let state =
3529            AbstractState::new().set("a", AbstractValue::from_constant(ConstantValue::Int(42)));
3530
3531        let val = parse_rhs_abstract("x = a", "x", &state, "python");
3532
3533        assert_eq!(val.range_, Some((Some(42), Some(42))));
3534        assert_eq!(val.constant, Some(ConstantValue::Int(42)));
3535    }
3536
3537    #[test]
3538    fn test_parse_rhs_abstract_simple_arithmetic() {
3539        // CAP-AI-13: Simple arithmetic x = a + 1
3540        let state =
3541            AbstractState::new().set("a", AbstractValue::from_constant(ConstantValue::Int(5)));
3542
3543        let val = parse_rhs_abstract("x = a + 3", "x", &state, "python");
3544
3545        assert_eq!(val.range_, Some((Some(8), Some(8))));
3546        assert_eq!(val.constant, Some(ConstantValue::Int(8)));
3547    }
3548
3549    #[test]
3550    fn test_parse_rhs_abstract_augmented_assignment() {
3551        // TIGER-PASS2-2: x += 1 treated as x = x + 1
3552        let state =
3553            AbstractState::new().set("x", AbstractValue::from_constant(ConstantValue::Int(10)));
3554
3555        let val = parse_rhs_abstract("x += 5", "x", &state, "python");
3556
3557        assert_eq!(val.range_, Some((Some(15), Some(15))));
3558        assert_eq!(val.constant, Some(ConstantValue::Int(15)));
3559    }
3560
3561    #[test]
3562    fn test_parse_rhs_abstract_with_comment() {
3563        // TIGER-PASS1-14: Comments stripped before parsing
3564        let state = AbstractState::new();
3565        let val = parse_rhs_abstract("x = 5  # this is the value", "x", &state, "python");
3566
3567        assert_eq!(val.range_, Some((Some(5), Some(5))));
3568    }
3569
3570    #[test]
3571    fn test_parse_rhs_abstract_unknown_returns_top() {
3572        // Unknown RHS returns top
3573        let state = AbstractState::new();
3574        let val = parse_rhs_abstract("x = foo(a, b)", "x", &state, "python");
3575
3576        // Should be top (unknown)
3577        assert_eq!(val.type_, None);
3578        assert_eq!(val.range_, None);
3579        assert_eq!(val.nullable, Nullability::Maybe);
3580    }
3581
3582    #[test]
3583    fn test_parse_rhs_abstract_no_assignment() {
3584        // Line doesn't contain assignment to this var
3585        let state = AbstractState::new();
3586        let val = parse_rhs_abstract("y = 5", "x", &state, "python");
3587
3588        // Should be top (unknown)
3589        assert_eq!(val.type_, None);
3590        assert_eq!(val.range_, None);
3591    }
3592
3593    // =========================================================================
3594    // Phase 10 Tests: compute_abstract_interp Main Algorithm
3595    // =========================================================================
3596
3597    use crate::types::{
3598        BlockType, CfgBlock, CfgEdge, CfgInfo, DfgInfo, EdgeType, VarRef,
3599    };
3600
3601    /// Helper to create a minimal CFG for testing
3602    fn make_test_cfg(function: &str, blocks: Vec<CfgBlock>, edges: Vec<CfgEdge>) -> CfgInfo {
3603        CfgInfo {
3604            function: function.to_string(),
3605            blocks,
3606            edges,
3607            entry_block: 0,
3608            exit_blocks: vec![0], // Simple case
3609            cyclomatic_complexity: 1,
3610            nested_functions: HashMap::new(),
3611        }
3612    }
3613
3614    /// Helper to create a VarRef
3615    fn make_var_ref(name: &str, ref_type: RefType, line: u32, column: u32) -> VarRef {
3616        VarRef {
3617            name: name.to_string(),
3618            ref_type,
3619            line,
3620            column,
3621            context: None,
3622            group_id: None,
3623        }
3624    }
3625
3626    #[test]
3627    fn test_compute_abstract_interp_returns_info() {
3628        // Basic: compute_abstract_interp returns AbstractInterpInfo
3629        let cfg = make_test_cfg(
3630            "test_func",
3631            vec![CfgBlock {
3632                id: 0,
3633                block_type: BlockType::Entry,
3634                lines: (1, 1),
3635                calls: vec![],
3636            }],
3637            vec![],
3638        );
3639        let dfg = DfgInfo {
3640            function: "test_func".to_string(),
3641            refs: vec![],
3642            edges: vec![],
3643            variables: vec![],
3644        };
3645
3646        let result = compute_abstract_interp(&cfg, &dfg, None, "python").unwrap();
3647        assert_eq!(result.function_name, "test_func");
3648    }
3649
3650    #[test]
3651    fn test_compute_tracks_constant_assignment() {
3652        // x = 5 should result in x having range [5, 5]
3653        let cfg = make_test_cfg(
3654            "const_test",
3655            vec![CfgBlock {
3656                id: 0,
3657                block_type: BlockType::Entry,
3658                lines: (1, 1),
3659                calls: vec![],
3660            }],
3661            vec![],
3662        );
3663        let dfg = DfgInfo {
3664            function: "const_test".to_string(),
3665            refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
3666            edges: vec![],
3667            variables: vec!["x".to_string()],
3668        };
3669        let source = ["x = 5"];
3670        let source_refs: Vec<&str> = source.to_vec();
3671
3672        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
3673        let val = result.value_at_exit(0, "x");
3674        assert_eq!(val.range_, Some((Some(5), Some(5))));
3675    }
3676
3677    #[test]
3678    fn test_compute_tracks_variable_copy() {
3679        // CAP-AI-19: y = x copies abstract value
3680        // x = 5
3681        // y = x  -> y should have same abstract value as x
3682        let cfg = make_test_cfg(
3683            "copy_test",
3684            vec![CfgBlock {
3685                id: 0,
3686                block_type: BlockType::Entry,
3687                lines: (1, 2),
3688                calls: vec![],
3689            }],
3690            vec![],
3691        );
3692        let dfg = DfgInfo {
3693            function: "copy_test".to_string(),
3694            refs: vec![
3695                make_var_ref("x", RefType::Definition, 1, 0),
3696                make_var_ref("x", RefType::Use, 2, 4),
3697                make_var_ref("y", RefType::Definition, 2, 0),
3698            ],
3699            edges: vec![],
3700            variables: vec!["x".to_string(), "y".to_string()],
3701        };
3702        let source = ["x = 5", "y = x"];
3703        let source_refs: Vec<&str> = source.to_vec();
3704
3705        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
3706        let val_x = result.value_at_exit(0, "x");
3707        let val_y = result.value_at_exit(0, "y");
3708        assert_eq!(val_x.range_, val_y.range_);
3709    }
3710
3711    #[test]
3712    fn test_compute_tracks_none_assignment() {
3713        // x = None should result in x being ALWAYS nullable
3714        let cfg = make_test_cfg(
3715            "none_test",
3716            vec![CfgBlock {
3717                id: 0,
3718                block_type: BlockType::Entry,
3719                lines: (1, 1),
3720                calls: vec![],
3721            }],
3722            vec![],
3723        );
3724        let dfg = DfgInfo {
3725            function: "none_test".to_string(),
3726            refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
3727            edges: vec![],
3728            variables: vec!["x".to_string()],
3729        };
3730        let source = ["x = None"];
3731        let source_refs: Vec<&str> = source.to_vec();
3732
3733        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
3734        let val = result.value_at_exit(0, "x");
3735        assert_eq!(val.nullable, Nullability::Always);
3736    }
3737
3738    #[test]
3739    fn test_abstract_interp_empty_function_no_crash() {
3740        // Empty function should not crash
3741        let cfg = make_test_cfg(
3742            "empty_func",
3743            vec![CfgBlock {
3744                id: 0,
3745                block_type: BlockType::Entry,
3746                lines: (1, 1),
3747                calls: vec![],
3748            }],
3749            vec![],
3750        );
3751        let dfg = DfgInfo {
3752            function: "empty_func".to_string(),
3753            refs: vec![],
3754            edges: vec![],
3755            variables: vec![],
3756        };
3757
3758        let result = compute_abstract_interp(&cfg, &dfg, None, "python");
3759        assert!(result.is_ok());
3760    }
3761
3762    #[test]
3763    fn test_unknown_rhs_defaults_to_top() {
3764        // Unknown RHS (e.g., function call) defaults to top()
3765        // x = some_unknown_function()
3766        let cfg = make_test_cfg(
3767            "unknown_test",
3768            vec![CfgBlock {
3769                id: 0,
3770                block_type: BlockType::Entry,
3771                lines: (1, 1),
3772                calls: vec![],
3773            }],
3774            vec![],
3775        );
3776        let dfg = DfgInfo {
3777            function: "unknown_test".to_string(),
3778            refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
3779            edges: vec![],
3780            variables: vec!["x".to_string()],
3781        };
3782        let source = ["x = some_function()"];
3783        let source_refs: Vec<&str> = source.to_vec();
3784
3785        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
3786        let val = result.value_at_exit(0, "x");
3787        // Should be top (unknown)
3788        assert_eq!(val.type_, None);
3789        assert_eq!(val.range_, None);
3790        assert_eq!(val.nullable, Nullability::Maybe);
3791    }
3792
3793    #[test]
3794    fn test_parameter_starts_as_top() {
3795        // Function parameters start as top() (unknown input)
3796        let cfg = make_test_cfg(
3797            "param_test",
3798            vec![CfgBlock {
3799                id: 0,
3800                block_type: BlockType::Entry,
3801                lines: (1, 1),
3802                calls: vec![],
3803            }],
3804            vec![],
3805        );
3806        let dfg = DfgInfo {
3807            function: "param_test".to_string(),
3808            refs: vec![make_var_ref("param", RefType::Definition, 1, 0)],
3809            edges: vec![],
3810            variables: vec!["param".to_string()],
3811        };
3812        // No source - just parameter definition with no assignment
3813        let result = compute_abstract_interp(&cfg, &dfg, None, "python").unwrap();
3814        let val = result.value_at(0, "param");
3815        // Parameters start as top (unknown)
3816        assert_eq!(val.type_, None);
3817        assert_eq!(val.range_, None);
3818        assert_eq!(val.nullable, Nullability::Maybe);
3819    }
3820
3821    #[test]
3822    fn test_nested_loops_terminate() {
3823        // Nested loops should terminate via widening
3824        // Create a CFG with nested loop structure
3825        let cfg = CfgInfo {
3826            function: "nested_loop".to_string(),
3827            blocks: vec![
3828                CfgBlock {
3829                    id: 0,
3830                    block_type: BlockType::Entry,
3831                    lines: (1, 1),
3832                    calls: vec![],
3833                },
3834                CfgBlock {
3835                    id: 1,
3836                    block_type: BlockType::LoopHeader,
3837                    lines: (2, 2),
3838                    calls: vec![],
3839                },
3840                CfgBlock {
3841                    id: 2,
3842                    block_type: BlockType::LoopHeader,
3843                    lines: (3, 3),
3844                    calls: vec![],
3845                },
3846                CfgBlock {
3847                    id: 3,
3848                    block_type: BlockType::LoopBody,
3849                    lines: (4, 4),
3850                    calls: vec![],
3851                },
3852                CfgBlock {
3853                    id: 4,
3854                    block_type: BlockType::Exit,
3855                    lines: (5, 5),
3856                    calls: vec![],
3857                },
3858            ],
3859            edges: vec![
3860                CfgEdge {
3861                    from: 0,
3862                    to: 1,
3863                    edge_type: EdgeType::Unconditional,
3864                    condition: None,
3865                },
3866                CfgEdge {
3867                    from: 1,
3868                    to: 2,
3869                    edge_type: EdgeType::True,
3870                    condition: Some("i < n".to_string()),
3871                },
3872                CfgEdge {
3873                    from: 1,
3874                    to: 4,
3875                    edge_type: EdgeType::False,
3876                    condition: None,
3877                },
3878                CfgEdge {
3879                    from: 2,
3880                    to: 3,
3881                    edge_type: EdgeType::True,
3882                    condition: Some("j < m".to_string()),
3883                },
3884                CfgEdge {
3885                    from: 2,
3886                    to: 1,
3887                    edge_type: EdgeType::False,
3888                    condition: None,
3889                },
3890                CfgEdge {
3891                    from: 3,
3892                    to: 2,
3893                    edge_type: EdgeType::BackEdge,
3894                    condition: None,
3895                },
3896            ],
3897            entry_block: 0,
3898            exit_blocks: vec![4],
3899            cyclomatic_complexity: 3,
3900            nested_functions: HashMap::new(),
3901        };
3902        let dfg = DfgInfo {
3903            function: "nested_loop".to_string(),
3904            refs: vec![
3905                make_var_ref("x", RefType::Definition, 4, 0),
3906                make_var_ref("x", RefType::Use, 4, 4),
3907            ],
3908            edges: vec![],
3909            variables: vec!["x".to_string()],
3910        };
3911        let source = ["x = 0",
3912            "for i in range(n):",
3913            "  for j in range(m):",
3914            "    x = x + 1",
3915            "return x"];
3916        let source_refs: Vec<&str> = source.to_vec();
3917
3918        // Should not infinite loop - widening ensures termination
3919        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python");
3920        assert!(result.is_ok());
3921    }
3922
3923    #[test]
3924    fn test_compute_accepts_language_parameter() {
3925        // compute_abstract_interp should accept language parameter
3926        let cfg = make_test_cfg(
3927            "lang_test",
3928            vec![CfgBlock {
3929                id: 0,
3930                block_type: BlockType::Entry,
3931                lines: (1, 1),
3932                calls: vec![],
3933            }],
3934            vec![],
3935        );
3936        let dfg = DfgInfo {
3937            function: "lang_test".to_string(),
3938            refs: vec![],
3939            edges: vec![],
3940            variables: vec![],
3941        };
3942
3943        // Both should succeed with different languages
3944        let result_py = compute_abstract_interp(&cfg, &dfg, None, "python");
3945        let result_ts = compute_abstract_interp(&cfg, &dfg, None, "typescript");
3946        assert!(result_py.is_ok());
3947        assert!(result_ts.is_ok());
3948    }
3949
3950    #[test]
3951    fn test_compute_with_typescript_null() {
3952        // TypeScript: let x = null;
3953        // Should recognize 'null' as null value
3954        let cfg = make_test_cfg(
3955            "ts_null_test",
3956            vec![CfgBlock {
3957                id: 0,
3958                block_type: BlockType::Entry,
3959                lines: (1, 1),
3960                calls: vec![],
3961            }],
3962            vec![],
3963        );
3964        let dfg = DfgInfo {
3965            function: "ts_null_test".to_string(),
3966            refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
3967            edges: vec![],
3968            variables: vec!["x".to_string()],
3969        };
3970        let source = ["let x = null"];
3971        let source_refs: Vec<&str> = source.to_vec();
3972
3973        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "typescript").unwrap();
3974        let val = result.value_at_exit(0, "x");
3975        assert_eq!(val.nullable, Nullability::Always);
3976    }
3977
3978    #[test]
3979    fn test_compute_with_go_nil() {
3980        // Go: x := nil
3981        // Should recognize 'nil' as null value
3982        let cfg = make_test_cfg(
3983            "go_nil_test",
3984            vec![CfgBlock {
3985                id: 0,
3986                block_type: BlockType::Entry,
3987                lines: (1, 1),
3988                calls: vec![],
3989            }],
3990            vec![],
3991        );
3992        let dfg = DfgInfo {
3993            function: "go_nil_test".to_string(),
3994            refs: vec![make_var_ref("x", RefType::Definition, 1, 0)],
3995            edges: vec![],
3996            variables: vec!["x".to_string()],
3997        };
3998        let source = ["x := nil"];
3999        let source_refs: Vec<&str> = source.to_vec();
4000
4001        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "go").unwrap();
4002        let val = result.value_at_exit(0, "x");
4003        assert_eq!(val.nullable, Nullability::Always);
4004    }
4005
4006    // =========================================================================
4007    // Phase 11 Tests: Division-by-Zero and Null Dereference Detection
4008    // =========================================================================
4009
4010    #[test]
4011    fn test_div_zero_detected_for_constant_zero() {
4012        // CAP-AI-10: x=0; y=1/x -> warning at y
4013        let cfg = make_test_cfg(
4014            "div_zero_const",
4015            vec![CfgBlock {
4016                id: 0,
4017                block_type: BlockType::Entry,
4018                lines: (1, 2),
4019                calls: vec![],
4020            }],
4021            vec![],
4022        );
4023        let dfg = DfgInfo {
4024            function: "div_zero_const".to_string(),
4025            refs: vec![
4026                make_var_ref("x", RefType::Definition, 1, 0),
4027                make_var_ref("x", RefType::Use, 2, 6),
4028                make_var_ref("y", RefType::Definition, 2, 0),
4029            ],
4030            edges: vec![],
4031            variables: vec!["x".to_string(), "y".to_string()],
4032        };
4033        let source = ["x = 0", "y = 1 / x"];
4034        let source_refs: Vec<&str> = source.to_vec();
4035
4036        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4037
4038        // Should detect division by zero at line 2
4039        assert!(
4040            !result.potential_div_zero.is_empty(),
4041            "Should detect division by zero"
4042        );
4043        assert!(
4044            result
4045                .potential_div_zero
4046                .iter()
4047                .any(|(line, var)| *line == 2 && var == "x"),
4048            "Should flag x at line 2 as potential div-by-zero"
4049        );
4050    }
4051
4052    #[test]
4053    fn test_div_zero_detected_for_range_including_zero() {
4054        // CAP-AI-10: Range [-5, 5] includes zero, should warn
4055        let cfg = make_test_cfg(
4056            "div_zero_range",
4057            vec![CfgBlock {
4058                id: 0,
4059                block_type: BlockType::Entry,
4060                lines: (1, 2),
4061                calls: vec![],
4062            }],
4063            vec![],
4064        );
4065        let dfg = DfgInfo {
4066            function: "div_zero_range".to_string(),
4067            refs: vec![
4068                make_var_ref("x", RefType::Definition, 1, 0), // x is unknown (top)
4069                make_var_ref("x", RefType::Use, 2, 6),
4070                make_var_ref("y", RefType::Definition, 2, 0),
4071            ],
4072            edges: vec![],
4073            variables: vec!["x".to_string(), "y".to_string()],
4074        };
4075        // x = foo() returns unknown value (could be zero)
4076        let source = ["x = foo()", "y = 1 / x"];
4077        let source_refs: Vec<&str> = source.to_vec();
4078
4079        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4080
4081        // Unknown value may be zero
4082        assert!(
4083            !result.potential_div_zero.is_empty(),
4084            "Should detect potential division by zero for unknown value"
4085        );
4086    }
4087
4088    #[test]
4089    fn test_div_safe_no_warning_for_constant_nonzero() {
4090        // CAP-AI-10: x=5; y=1/x -> NO warning
4091        let cfg = make_test_cfg(
4092            "div_safe_const",
4093            vec![CfgBlock {
4094                id: 0,
4095                block_type: BlockType::Entry,
4096                lines: (1, 2),
4097                calls: vec![],
4098            }],
4099            vec![],
4100        );
4101        let dfg = DfgInfo {
4102            function: "div_safe_const".to_string(),
4103            refs: vec![
4104                make_var_ref("x", RefType::Definition, 1, 0),
4105                make_var_ref("x", RefType::Use, 2, 6),
4106                make_var_ref("y", RefType::Definition, 2, 0),
4107            ],
4108            edges: vec![],
4109            variables: vec!["x".to_string(), "y".to_string()],
4110        };
4111        let source = ["x = 5", "y = 1 / x"];
4112        let source_refs: Vec<&str> = source.to_vec();
4113
4114        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4115
4116        // x = 5, definitely not zero
4117        assert!(
4118            result.potential_div_zero.is_empty()
4119                || !result
4120                    .potential_div_zero
4121                    .iter()
4122                    .any(|(line, var)| *line == 2 && var == "x"),
4123            "Should NOT warn for division by constant non-zero"
4124        );
4125    }
4126
4127    #[test]
4128    fn test_div_safe_no_warning_for_positive_range() {
4129        // CAP-AI-10: x=5; x=x+1; y=1/x -> NO warning (range [6,6])
4130        let cfg = make_test_cfg(
4131            "div_safe_range",
4132            vec![CfgBlock {
4133                id: 0,
4134                block_type: BlockType::Entry,
4135                lines: (1, 3),
4136                calls: vec![],
4137            }],
4138            vec![],
4139        );
4140        let dfg = DfgInfo {
4141            function: "div_safe_range".to_string(),
4142            refs: vec![
4143                make_var_ref("x", RefType::Definition, 1, 0),
4144                make_var_ref("x", RefType::Use, 2, 4),
4145                make_var_ref("x", RefType::Definition, 2, 0),
4146                make_var_ref("x", RefType::Use, 3, 6),
4147                make_var_ref("y", RefType::Definition, 3, 0),
4148            ],
4149            edges: vec![],
4150            variables: vec!["x".to_string(), "y".to_string()],
4151        };
4152        let source = ["x = 5", "x = x + 1", "y = 1 / x"];
4153        let source_refs: Vec<&str> = source.to_vec();
4154
4155        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4156
4157        // x has range [6, 6], definitely not zero
4158        assert!(
4159            result.potential_div_zero.is_empty()
4160                || !result
4161                    .potential_div_zero
4162                    .iter()
4163                    .any(|(line, var)| *line == 3 && var == "x"),
4164            "Should NOT warn for positive range that excludes zero"
4165        );
4166    }
4167
4168    #[test]
4169    fn test_div_zero_intra_block_accuracy() {
4170        // CAP-AI-20 / TIGER-PASS1-13: Intra-block precision
4171        // x = 0       # line 1
4172        // x = 5       # line 2 - redefined to non-zero
4173        // y = 1 / x   # line 3 - should NOT warn (x is 5 at this point)
4174        let cfg = make_test_cfg(
4175            "div_intra_block",
4176            vec![CfgBlock {
4177                id: 0,
4178                block_type: BlockType::Entry,
4179                lines: (1, 3),
4180                calls: vec![],
4181            }],
4182            vec![],
4183        );
4184        let dfg = DfgInfo {
4185            function: "div_intra_block".to_string(),
4186            refs: vec![
4187                make_var_ref("x", RefType::Definition, 1, 0),
4188                make_var_ref("x", RefType::Definition, 2, 0), // Redefined
4189                make_var_ref("x", RefType::Use, 3, 6),
4190                make_var_ref("y", RefType::Definition, 3, 0),
4191            ],
4192            edges: vec![],
4193            variables: vec!["x".to_string(), "y".to_string()],
4194        };
4195        let source = ["x = 0", "x = 5", "y = 1 / x"];
4196        let source_refs: Vec<&str> = source.to_vec();
4197
4198        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4199
4200        // At line 3, x should be 5 (not 0), so no warning
4201        // This tests intra-block precision
4202        assert!(result.potential_div_zero.is_empty() ||
4203                !result.potential_div_zero.iter().any(|(line, var)| *line == 3 && var == "x"),
4204            "Should NOT warn when divisor is redefined to non-zero before division (intra-block precision)");
4205    }
4206
4207    #[test]
4208    fn test_div_zero_not_triggered_by_path_strings() {
4209        // Regression: Path::new("/projects/myapp") was flagged as division
4210        // because the `/` in string literals was not stripped.
4211        let cfg = make_test_cfg(
4212            "path_strings",
4213            vec![CfgBlock {
4214                id: 0,
4215                block_type: BlockType::Entry,
4216                lines: (1, 2),
4217                calls: vec![],
4218            }],
4219            vec![],
4220        );
4221        let dfg = DfgInfo {
4222            function: "path_strings".to_string(),
4223            refs: vec![
4224                make_var_ref("root", RefType::Definition, 1, 0),
4225                make_var_ref("child", RefType::Definition, 2, 0),
4226            ],
4227            edges: vec![],
4228            variables: vec!["root".to_string(), "child".to_string()],
4229        };
4230        let source = ["root = \"/projects/myapp\"",
4231            "child = \"/src/main.rs\""];
4232        let source_refs: Vec<&str> = source.to_vec();
4233
4234        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4235
4236        assert!(
4237            result.potential_div_zero.is_empty(),
4238            "Path separators inside string literals must not trigger div-by-zero; got: {:?}",
4239            result.potential_div_zero
4240        );
4241    }
4242
4243    #[test]
4244    fn test_div_zero_still_detects_real_division_with_strings() {
4245        // Real division must still be detected even when string paths are on same line
4246        let cfg = make_test_cfg(
4247            "mixed_strings_div",
4248            vec![CfgBlock {
4249                id: 0,
4250                block_type: BlockType::Entry,
4251                lines: (1, 3),
4252                calls: vec![],
4253            }],
4254            vec![],
4255        );
4256        let dfg = DfgInfo {
4257            function: "mixed_strings_div".to_string(),
4258            refs: vec![
4259                make_var_ref("path", RefType::Definition, 1, 0),
4260                make_var_ref("x", RefType::Definition, 2, 0),
4261                make_var_ref("y", RefType::Definition, 3, 0),
4262                make_var_ref("x", RefType::Use, 3, 10),
4263            ],
4264            edges: vec![],
4265            variables: vec!["path".to_string(), "x".to_string(), "y".to_string()],
4266        };
4267        let source = ["path = \"/src/main.rs\"",
4268            "x = foo()",
4269            "y = 100 / x"];
4270        let source_refs: Vec<&str> = source.to_vec();
4271
4272        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4273
4274        assert!(
4275            result.potential_div_zero.iter().any(|(line, var)| *line == 3 && var == "x"),
4276            "Real division by unknown x should still be flagged; got: {:?}",
4277            result.potential_div_zero
4278        );
4279        // And no FP from the path string
4280        assert!(
4281            !result.potential_div_zero.iter().any(|(_, var)| var == "main" || var == "src"),
4282            "Path components in strings must not be flagged; got: {:?}",
4283            result.potential_div_zero
4284        );
4285    }
4286
4287    #[test]
4288    fn test_null_deref_detected_at_attribute_access() {
4289        // CAP-AI-11: x=None; y=x.foo -> warning at y
4290        let cfg = make_test_cfg(
4291            "null_deref",
4292            vec![CfgBlock {
4293                id: 0,
4294                block_type: BlockType::Entry,
4295                lines: (1, 2),
4296                calls: vec![],
4297            }],
4298            vec![],
4299        );
4300        let dfg = DfgInfo {
4301            function: "null_deref".to_string(),
4302            refs: vec![
4303                make_var_ref("x", RefType::Definition, 1, 0),
4304                make_var_ref("x", RefType::Use, 2, 4),
4305                make_var_ref("y", RefType::Definition, 2, 0),
4306            ],
4307            edges: vec![],
4308            variables: vec!["x".to_string(), "y".to_string()],
4309        };
4310        let source = ["x = None", "y = x.foo"];
4311        let source_refs: Vec<&str> = source.to_vec();
4312
4313        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4314
4315        // Should detect null dereference
4316        assert!(
4317            !result.potential_null_deref.is_empty(),
4318            "Should detect null dereference"
4319        );
4320        assert!(
4321            result
4322                .potential_null_deref
4323                .iter()
4324                .any(|(line, var)| *line == 2 && var == "x"),
4325            "Should flag x at line 2 as potential null deref"
4326        );
4327    }
4328
4329    #[test]
4330    fn test_null_deref_safe_for_non_null_constant() {
4331        // CAP-AI-11: x='hello'; y=x.upper() -> NO warning
4332        let cfg = make_test_cfg(
4333            "null_safe",
4334            vec![CfgBlock {
4335                id: 0,
4336                block_type: BlockType::Entry,
4337                lines: (1, 2),
4338                calls: vec![],
4339            }],
4340            vec![],
4341        );
4342        let dfg = DfgInfo {
4343            function: "null_safe".to_string(),
4344            refs: vec![
4345                make_var_ref("x", RefType::Definition, 1, 0),
4346                make_var_ref("x", RefType::Use, 2, 4),
4347                make_var_ref("y", RefType::Definition, 2, 0),
4348            ],
4349            edges: vec![],
4350            variables: vec!["x".to_string(), "y".to_string()],
4351        };
4352        let source = ["x = 'hello'", "y = x.upper()"];
4353        let source_refs: Vec<&str> = source.to_vec();
4354
4355        let result = compute_abstract_interp(&cfg, &dfg, Some(&source_refs), "python").unwrap();
4356
4357        // String constant is not null
4358        assert!(
4359            result.potential_null_deref.is_empty()
4360                || !result
4361                    .potential_null_deref
4362                    .iter()
4363                    .any(|(line, var)| *line == 2 && var == "x"),
4364            "Should NOT warn for dereference of non-null constant"
4365        );
4366    }
4367}