Skip to main content

torsh_jit/abstract_interpretation/
framework.rs

1//! Core Abstract Interpretation Framework
2//!
3//! This module contains the main AbstractInterpreter engine that orchestrates
4//! abstract interpretation analysis, including forward/backward analysis,
5//! invariant detection, and property checking.
6
7use super::domains::{
8    AbstractDomain, AbstractDomainFactory, AbstractValue, BinaryAbstractOp,
9    ConstantValue as DomainConstantValue, SignValue, UnaryAbstractOp,
10};
11use crate::graph::operations::{ConstantValue as OpConstantValue, Operation};
12use crate::{
13    ir::{BasicBlock, BlockId, IrModule, IrOpcode, IrValue},
14    ComputationGraph, JitResult, NodeId,
15};
16use std::collections::{HashMap, HashSet, VecDeque};
17use std::time::Instant;
18
19#[path = "framework_config.rs"]
20mod framework_config;
21pub use framework_config::*;
22
23/// Abstract interpretation engine for static analysis
24pub struct AbstractInterpreter {
25    config: AbstractInterpretationConfig,
26    domain_factory: AbstractDomainFactory,
27    fixpoint_engine: FixpointEngine,
28    invariant_detector: InvariantDetector,
29    property_checker: PropertyChecker,
30    analysis_cache: AnalysisCache,
31}
32
33impl AbstractInterpreter {
34    /// Create a new abstract interpreter with specified configuration
35    ///
36    /// # Arguments
37    /// * `config` - Configuration for the abstract interpretation
38    ///
39    /// # Returns
40    /// * `Self` - New AbstractInterpreter instance
41    pub fn new(config: AbstractInterpretationConfig) -> Self {
42        Self {
43            domain_factory: AbstractDomainFactory::new(),
44            fixpoint_engine: FixpointEngine::new(config.clone()),
45            invariant_detector: InvariantDetector::new(),
46            property_checker: PropertyChecker::new(),
47            analysis_cache: AnalysisCache::new(),
48            config,
49        }
50    }
51
52    /// Create an interpreter with default configuration
53    pub fn with_defaults() -> Self {
54        Self::new(AbstractInterpretationConfig::default())
55    }
56
57    /// Perform abstract interpretation on a computation graph
58    ///
59    /// This is the main entry point for analyzing computation graphs.
60    /// It orchestrates the entire analysis process including forward/backward
61    /// analysis, invariant detection, and property checking.
62    ///
63    /// # Arguments
64    /// * `graph` - The computation graph to analyze
65    ///
66    /// # Returns
67    /// * `JitResult<AbstractAnalysisResult>` - Complete analysis results
68    pub fn analyze_graph(&mut self, graph: &ComputationGraph) -> JitResult<AbstractAnalysisResult> {
69        let start_time = Instant::now();
70
71        // Create abstract domain for the analysis
72        let domain = self.domain_factory.create_domain(&self.config.domain_type);
73
74        // Convert graph to abstract representation
75        let abstract_graph = self.convert_to_abstract_graph(graph, domain.as_ref())?;
76
77        // Perform forward analysis
78        let forward_result = self.forward_analysis(&abstract_graph, domain.as_ref())?;
79
80        // Perform backward analysis if enabled
81        let backward_result = if self.config.enable_backward_analysis {
82            Some(self.backward_analysis(&abstract_graph, domain.as_ref())?)
83        } else {
84            None
85        };
86
87        // Detect invariants
88        let invariants = self
89            .invariant_detector
90            .detect_invariants(&forward_result, &backward_result)?;
91
92        // Check properties
93        let property_results = self
94            .property_checker
95            .check_properties(&forward_result, &self.config.properties)?;
96
97        // Analyze precision and performance
98        let precision_analysis = self.analyze_precision(&forward_result, &abstract_graph);
99        let performance_analysis = self.analyze_performance(&forward_result, graph);
100
101        let total_time = start_time.elapsed();
102        let fixpoint_iterations =
103            forward_result.iterations + backward_result.as_ref().map(|r| r.iterations).unwrap_or(0);
104        let abstract_states_computed = forward_result.post_states.len();
105        let node_values = forward_result.post_states.clone();
106
107        Ok(AbstractAnalysisResult {
108            forward_result,
109            backward_result,
110            invariants,
111            property_results,
112            precision_analysis,
113            performance_analysis,
114            statistics: AnalysisStatistics {
115                analysis_time: total_time,
116                fixpoint_iterations,
117                abstract_states_computed,
118                cache_hits: self.analysis_cache.hit_count(),
119                cache_misses: self.analysis_cache.miss_count(),
120            },
121            node_values,
122        })
123    }
124
125    /// Perform abstract interpretation on an IR module
126    ///
127    /// Analyzes an intermediate representation module, processing
128    /// basic blocks and performing interprocedural analysis.
129    ///
130    /// # Arguments
131    /// * `ir_module` - The IR module to analyze
132    ///
133    /// # Returns
134    /// * `JitResult<AbstractIrResult>` - IR-specific analysis results
135    pub fn analyze_ir(&mut self, ir_module: &IrModule) -> JitResult<AbstractIrResult> {
136        let mut block_results = HashMap::new();
137        let mut module_invariants = Vec::new();
138
139        // Analyze each basic block
140        for (block_id, block) in &ir_module.blocks {
141            let block_result = self.analyze_block(block)?;
142            block_results.insert(*block_id, block_result);
143        }
144
145        // Perform interprocedural analysis
146        let interprocedural_result = self.interprocedural_analysis_blocks(&block_results)?;
147
148        // Detect module-level invariants
149        module_invariants.extend(self.detect_module_invariants_blocks(&block_results)?);
150
151        Ok(AbstractIrResult {
152            function_results: HashMap::new(), // No functions in this IR model
153            interprocedural_result,
154            module_invariants,
155        })
156    }
157
158    /// Get the current configuration
159    pub fn config(&self) -> &AbstractInterpretationConfig {
160        &self.config
161    }
162
163    /// Update the configuration
164    pub fn set_config(&mut self, config: AbstractInterpretationConfig) {
165        self.config = config;
166        self.fixpoint_engine = FixpointEngine::new(self.config.clone());
167    }
168
169    /// Clear the analysis cache
170    pub fn clear_cache(&mut self) {
171        self.analysis_cache.clear();
172    }
173
174    /// Get cache statistics
175    pub fn cache_stats(&self) -> (usize, usize) {
176        (
177            self.analysis_cache.hit_count(),
178            self.analysis_cache.miss_count(),
179        )
180    }
181}
182
183// Placeholder implementations for engine components
184// These would be implemented in separate modules
185
186/// Fixpoint computation engine
187pub struct FixpointEngine {
188    config: AbstractInterpretationConfig,
189}
190
191impl FixpointEngine {
192    pub fn new(config: AbstractInterpretationConfig) -> Self {
193        Self { config }
194    }
195}
196
197/// Invariant detection engine.
198///
199/// Walks the post-states of a forward analysis (and the union with the
200/// backward analysis if available) and reports an [`Invariant`] for
201/// every node whose abstract value is precise enough to constrain the
202/// concrete semantics — singleton intervals, pure signs, exact
203/// constants, etc.
204pub struct InvariantDetector;
205
206impl InvariantDetector {
207    pub fn new() -> Self {
208        Self
209    }
210
211    /// Detect invariants from forward (and optional backward) results.
212    ///
213    /// The detector emits:
214    /// - `NumericalProperty` for nodes with constant abstract value;
215    /// - `ValueRange` for nodes pinned to a finite, non-singleton interval;
216    /// - `MemorySafety` for nodes whose sign domain rules out a sign
217    ///   (e.g. `NonNegative` rules out negative values).
218    pub fn detect_invariants(
219        &self,
220        forward_result: &ForwardAnalysisResult,
221        backward_result: &Option<BackwardAnalysisResult>,
222    ) -> JitResult<Vec<Invariant>> {
223        let mut invariants = Vec::new();
224        for (node_id, value) in &forward_result.post_states {
225            if let Some(inv) = describe_invariant(*node_id, value, "forward") {
226                invariants.push(inv);
227            }
228        }
229        if let Some(bw) = backward_result {
230            for (node_id, value) in &bw.pre_states {
231                // Avoid duplicates with forward by tagging the source.
232                if let Some(inv) = describe_invariant(*node_id, value, "backward") {
233                    invariants.push(inv);
234                }
235            }
236        }
237        Ok(invariants)
238    }
239}
240
241/// Build an [`Invariant`] from a single abstract value, or return
242/// `None` if the value is too imprecise to constrain anything.
243fn describe_invariant(node_id: NodeId, value: &AbstractValue, source: &str) -> Option<Invariant> {
244    match value {
245        AbstractValue::Interval { min, max } => {
246            if min == max && min.is_finite() {
247                Some(Invariant {
248                    invariant_type: InvariantType::NumericalProperty,
249                    description: format!("node {:?} = {}", node_id, min),
250                    confidence: 1.0,
251                    location: source.to_string(),
252                })
253            } else if min.is_finite() && max.is_finite() {
254                Some(Invariant {
255                    invariant_type: InvariantType::ValueRange,
256                    description: format!("node {:?} ∈ [{}, {}]", node_id, min, max),
257                    confidence: 0.9,
258                    location: source.to_string(),
259                })
260            } else {
261                None
262            }
263        }
264        AbstractValue::Constant(DomainConstantValue::Value(v)) => Some(Invariant {
265            invariant_type: InvariantType::NumericalProperty,
266            description: format!("node {:?} ≡ {}", node_id, v),
267            confidence: 1.0,
268            location: source.to_string(),
269        }),
270        AbstractValue::Sign(SignValue::Zero) => Some(Invariant {
271            invariant_type: InvariantType::NumericalProperty,
272            description: format!("node {:?} ≡ 0", node_id),
273            confidence: 1.0,
274            location: source.to_string(),
275        }),
276        AbstractValue::Sign(SignValue::NonNegative) => Some(Invariant {
277            invariant_type: InvariantType::MemorySafety,
278            description: format!("node {:?} ≥ 0", node_id),
279            confidence: 0.95,
280            location: source.to_string(),
281        }),
282        AbstractValue::Sign(SignValue::Positive) => Some(Invariant {
283            invariant_type: InvariantType::MemorySafety,
284            description: format!("node {:?} > 0", node_id),
285            confidence: 0.95,
286            location: source.to_string(),
287        }),
288        _ => None,
289    }
290}
291
292impl Default for InvariantDetector {
293    fn default() -> Self {
294        Self::new()
295    }
296}
297
298/// Property checking engine.
299///
300/// Verifies safety [`Property`] values against the post-states produced
301/// by a forward analysis. Each check returns a [`PropertyResult`]
302/// classifying the property as `Safe`, `Unsafe`, or `Unknown` along
303/// with an analyst-readable explanation.
304pub struct PropertyChecker;
305
306impl PropertyChecker {
307    pub fn new() -> Self {
308        Self
309    }
310
311    /// Check every property against the corresponding node's abstract
312    /// post-state.
313    ///
314    /// The interpretation of "safe" vs. "unsafe" depends on the
315    /// property and the abstract domain:
316    /// - For interval domains, a property is `Safe` iff every concrete
317    ///   value reachable through the abstraction satisfies it.
318    /// - For sign domains, signs that strictly imply the property are
319    ///   `Safe`; signs that exclude it are `Unsafe`; the rest are
320    ///   `Unknown`.
321    pub fn check_properties(
322        &self,
323        forward_result: &ForwardAnalysisResult,
324        properties: &[Property],
325    ) -> JitResult<Vec<PropertyResult>> {
326        let mut results = Vec::with_capacity(properties.len());
327        for prop in properties {
328            let node = property_node(prop);
329            let state = forward_result.post_states.get(&node);
330            results.push(check_property(prop, state));
331        }
332        Ok(results)
333    }
334}
335
336/// Extract the [`NodeId`] guarded by a [`Property`].
337fn property_node(prop: &Property) -> NodeId {
338    match prop {
339        Property::NonNegative(n)
340        | Property::Positive(n)
341        | Property::BoundedValue(n, _, _)
342        | Property::NoDivisionByZero(n)
343        | Property::NoOverflow(n)
344        | Property::SafetyProperty(_, n) => *n,
345    }
346}
347
348/// Decide a single property against its abstract value.
349fn check_property(prop: &Property, value: Option<&AbstractValue>) -> PropertyResult {
350    let value = match value {
351        Some(v) => v,
352        None => {
353            return PropertyResult {
354                property: prop.clone(),
355                result: SafetyCheckResult::Unknown,
356                confidence: 0.0,
357                details: "no abstract value computed for node".to_string(),
358            }
359        }
360    };
361    match prop {
362        Property::NonNegative(_) => decide_non_negative(prop, value),
363        Property::Positive(_) => decide_positive(prop, value),
364        Property::BoundedValue(_, lo, hi) => decide_bounded(prop, value, *lo, *hi),
365        Property::NoDivisionByZero(_) => decide_nonzero(prop, value),
366        Property::NoOverflow(_) => decide_no_overflow(prop, value),
367        Property::SafetyProperty(_, _) => PropertyResult {
368            property: prop.clone(),
369            result: SafetyCheckResult::Unknown,
370            confidence: 0.0,
371            details: "custom safety property requires explicit verifier".to_string(),
372        },
373    }
374}
375
376fn decide_non_negative(prop: &Property, value: &AbstractValue) -> PropertyResult {
377    match value {
378        AbstractValue::Interval { min, max } => {
379            if *min >= 0.0 {
380                PropertyResult {
381                    property: prop.clone(),
382                    result: SafetyCheckResult::Safe,
383                    confidence: 1.0,
384                    details: "interval lower bound ≥ 0".to_string(),
385                }
386            } else if *max < 0.0 {
387                PropertyResult {
388                    property: prop.clone(),
389                    result: SafetyCheckResult::Unsafe,
390                    confidence: 1.0,
391                    details: "interval upper bound < 0".to_string(),
392                }
393            } else {
394                PropertyResult {
395                    property: prop.clone(),
396                    result: SafetyCheckResult::Unknown,
397                    confidence: 0.5,
398                    details: "interval straddles zero".to_string(),
399                }
400            }
401        }
402        AbstractValue::Sign(s) => match s {
403            SignValue::Zero | SignValue::Positive | SignValue::NonNegative => PropertyResult {
404                property: prop.clone(),
405                result: SafetyCheckResult::Safe,
406                confidence: 1.0,
407                details: "sign domain proves non-negativity".to_string(),
408            },
409            SignValue::Negative => PropertyResult {
410                property: prop.clone(),
411                result: SafetyCheckResult::Unsafe,
412                confidence: 1.0,
413                details: "sign domain proves negativity".to_string(),
414            },
415            _ => PropertyResult {
416                property: prop.clone(),
417                result: SafetyCheckResult::Unknown,
418                confidence: 0.5,
419                details: "sign domain too imprecise".to_string(),
420            },
421        },
422        AbstractValue::Constant(DomainConstantValue::Value(v)) => {
423            if *v >= 0.0 {
424                PropertyResult {
425                    property: prop.clone(),
426                    result: SafetyCheckResult::Safe,
427                    confidence: 1.0,
428                    details: format!("constant {} ≥ 0", v),
429                }
430            } else {
431                PropertyResult {
432                    property: prop.clone(),
433                    result: SafetyCheckResult::Unsafe,
434                    confidence: 1.0,
435                    details: format!("constant {} < 0", v),
436                }
437            }
438        }
439        _ => PropertyResult {
440            property: prop.clone(),
441            result: SafetyCheckResult::Unknown,
442            confidence: 0.0,
443            details: "abstract domain insufficient".to_string(),
444        },
445    }
446}
447
448fn decide_positive(prop: &Property, value: &AbstractValue) -> PropertyResult {
449    match value {
450        AbstractValue::Interval { min, max } => {
451            if *min > 0.0 {
452                PropertyResult {
453                    property: prop.clone(),
454                    result: SafetyCheckResult::Safe,
455                    confidence: 1.0,
456                    details: "interval strictly positive".to_string(),
457                }
458            } else if *max <= 0.0 {
459                PropertyResult {
460                    property: prop.clone(),
461                    result: SafetyCheckResult::Unsafe,
462                    confidence: 1.0,
463                    details: "interval entirely ≤ 0".to_string(),
464                }
465            } else {
466                PropertyResult {
467                    property: prop.clone(),
468                    result: SafetyCheckResult::Unknown,
469                    confidence: 0.5,
470                    details: "interval includes 0 or negatives".to_string(),
471                }
472            }
473        }
474        AbstractValue::Sign(SignValue::Positive) => PropertyResult {
475            property: prop.clone(),
476            result: SafetyCheckResult::Safe,
477            confidence: 1.0,
478            details: "sign domain proves positivity".to_string(),
479        },
480        AbstractValue::Sign(SignValue::Zero)
481        | AbstractValue::Sign(SignValue::Negative)
482        | AbstractValue::Sign(SignValue::NonPositive) => PropertyResult {
483            property: prop.clone(),
484            result: SafetyCheckResult::Unsafe,
485            confidence: 1.0,
486            details: "sign domain proves non-positivity".to_string(),
487        },
488        AbstractValue::Constant(DomainConstantValue::Value(v)) => {
489            if *v > 0.0 {
490                PropertyResult {
491                    property: prop.clone(),
492                    result: SafetyCheckResult::Safe,
493                    confidence: 1.0,
494                    details: format!("constant {} > 0", v),
495                }
496            } else {
497                PropertyResult {
498                    property: prop.clone(),
499                    result: SafetyCheckResult::Unsafe,
500                    confidence: 1.0,
501                    details: format!("constant {} ≤ 0", v),
502                }
503            }
504        }
505        _ => PropertyResult {
506            property: prop.clone(),
507            result: SafetyCheckResult::Unknown,
508            confidence: 0.0,
509            details: "abstract domain insufficient".to_string(),
510        },
511    }
512}
513
514fn decide_bounded(prop: &Property, value: &AbstractValue, lo: f64, hi: f64) -> PropertyResult {
515    match value {
516        AbstractValue::Interval { min, max } => {
517            if *min >= lo && *max <= hi {
518                PropertyResult {
519                    property: prop.clone(),
520                    result: SafetyCheckResult::Safe,
521                    confidence: 1.0,
522                    details: format!("interval [{}, {}] ⊆ [{}, {}]", min, max, lo, hi),
523                }
524            } else if *max < lo || *min > hi {
525                PropertyResult {
526                    property: prop.clone(),
527                    result: SafetyCheckResult::Unsafe,
528                    confidence: 1.0,
529                    details: format!("interval disjoint from [{}, {}]", lo, hi),
530                }
531            } else {
532                PropertyResult {
533                    property: prop.clone(),
534                    result: SafetyCheckResult::Unknown,
535                    confidence: 0.5,
536                    details: format!(
537                        "interval [{}, {}] partially exceeds [{}, {}]",
538                        min, max, lo, hi
539                    ),
540                }
541            }
542        }
543        AbstractValue::Constant(DomainConstantValue::Value(v)) => {
544            if *v >= lo && *v <= hi {
545                PropertyResult {
546                    property: prop.clone(),
547                    result: SafetyCheckResult::Safe,
548                    confidence: 1.0,
549                    details: format!("constant {} ∈ [{}, {}]", v, lo, hi),
550                }
551            } else {
552                PropertyResult {
553                    property: prop.clone(),
554                    result: SafetyCheckResult::Unsafe,
555                    confidence: 1.0,
556                    details: format!("constant {} ∉ [{}, {}]", v, lo, hi),
557                }
558            }
559        }
560        _ => PropertyResult {
561            property: prop.clone(),
562            result: SafetyCheckResult::Unknown,
563            confidence: 0.0,
564            details: "abstract domain insufficient for bounded check".to_string(),
565        },
566    }
567}
568
569fn decide_nonzero(prop: &Property, value: &AbstractValue) -> PropertyResult {
570    match value {
571        AbstractValue::Interval { min, max } => {
572            if *min > 0.0 || *max < 0.0 {
573                PropertyResult {
574                    property: prop.clone(),
575                    result: SafetyCheckResult::Safe,
576                    confidence: 1.0,
577                    details: "interval excludes zero".to_string(),
578                }
579            } else if *min == 0.0 && *max == 0.0 {
580                PropertyResult {
581                    property: prop.clone(),
582                    result: SafetyCheckResult::Unsafe,
583                    confidence: 1.0,
584                    details: "interval is {0}".to_string(),
585                }
586            } else {
587                PropertyResult {
588                    property: prop.clone(),
589                    result: SafetyCheckResult::Unknown,
590                    confidence: 0.5,
591                    details: "interval contains zero".to_string(),
592                }
593            }
594        }
595        AbstractValue::Sign(SignValue::Zero) => PropertyResult {
596            property: prop.clone(),
597            result: SafetyCheckResult::Unsafe,
598            confidence: 1.0,
599            details: "sign domain proves value is zero".to_string(),
600        },
601        AbstractValue::Sign(SignValue::Positive) | AbstractValue::Sign(SignValue::Negative) => {
602            PropertyResult {
603                property: prop.clone(),
604                result: SafetyCheckResult::Safe,
605                confidence: 1.0,
606                details: "sign domain proves non-zero".to_string(),
607            }
608        }
609        AbstractValue::Constant(DomainConstantValue::Value(v)) => {
610            if *v != 0.0 {
611                PropertyResult {
612                    property: prop.clone(),
613                    result: SafetyCheckResult::Safe,
614                    confidence: 1.0,
615                    details: format!("constant {} ≠ 0", v),
616                }
617            } else {
618                PropertyResult {
619                    property: prop.clone(),
620                    result: SafetyCheckResult::Unsafe,
621                    confidence: 1.0,
622                    details: "constant is zero".to_string(),
623                }
624            }
625        }
626        _ => PropertyResult {
627            property: prop.clone(),
628            result: SafetyCheckResult::Unknown,
629            confidence: 0.0,
630            details: "abstract domain insufficient".to_string(),
631        },
632    }
633}
634
635fn decide_no_overflow(prop: &Property, value: &AbstractValue) -> PropertyResult {
636    match value {
637        AbstractValue::Interval { min, max } => {
638            if min.is_finite() && max.is_finite() {
639                PropertyResult {
640                    property: prop.clone(),
641                    result: SafetyCheckResult::Safe,
642                    confidence: 1.0,
643                    details: "interval is finite".to_string(),
644                }
645            } else {
646                PropertyResult {
647                    property: prop.clone(),
648                    result: SafetyCheckResult::Unknown,
649                    confidence: 0.3,
650                    details: "interval is unbounded".to_string(),
651                }
652            }
653        }
654        AbstractValue::Constant(DomainConstantValue::Value(v)) => {
655            if v.is_finite() {
656                PropertyResult {
657                    property: prop.clone(),
658                    result: SafetyCheckResult::Safe,
659                    confidence: 1.0,
660                    details: format!("constant {} is finite", v),
661                }
662            } else {
663                PropertyResult {
664                    property: prop.clone(),
665                    result: SafetyCheckResult::Unsafe,
666                    confidence: 1.0,
667                    details: "constant is not finite".to_string(),
668                }
669            }
670        }
671        _ => PropertyResult {
672            property: prop.clone(),
673            result: SafetyCheckResult::Unknown,
674            confidence: 0.3,
675            details: "abstract domain insufficient for overflow check".to_string(),
676        },
677    }
678}
679
680impl Default for PropertyChecker {
681    fn default() -> Self {
682        Self::new()
683    }
684}
685
686/// Analysis result cache
687pub struct AnalysisCache {
688    hits: usize,
689    misses: usize,
690}
691
692impl AnalysisCache {
693    pub fn new() -> Self {
694        Self { hits: 0, misses: 0 }
695    }
696
697    pub fn hit_count(&self) -> usize {
698        self.hits
699    }
700
701    pub fn miss_count(&self) -> usize {
702        self.misses
703    }
704
705    pub fn clear(&mut self) {
706        self.hits = 0;
707        self.misses = 0;
708    }
709}
710
711impl Default for AnalysisCache {
712    fn default() -> Self {
713        Self::new()
714    }
715}
716
717// Real analysis implementations
718impl AbstractInterpreter {
719    /// Translate a [`ComputationGraph`] into an [`AbstractGraph`] suitable
720    /// for fixed-point analysis.
721    ///
722    /// The translation is a structural pass: every concrete node is
723    /// classified into an [`AbstractNodeOp`] using
724    /// [`Self::classify_operation`], and successor/predecessor adjacency
725    /// is mirrored from the source graph. Entry nodes are nodes with no
726    /// predecessors; exit nodes are those with no successors.
727    fn convert_to_abstract_graph(
728        &self,
729        graph: &ComputationGraph,
730        _domain: &dyn AbstractDomain,
731    ) -> JitResult<AbstractGraph> {
732        let mut abstract_graph = AbstractGraph::new();
733        for (node_id, node) in graph.nodes() {
734            let op = Self::classify_operation(&node.operation);
735            abstract_graph.node_ops.insert(node_id, op);
736            let preds = graph.get_node_inputs(node_id);
737            let succs = graph.get_node_outputs(node_id);
738            abstract_graph.predecessors.insert(node_id, preds);
739            abstract_graph.successors.insert(node_id, succs);
740        }
741        for node_id in abstract_graph.node_ops.keys().copied().collect::<Vec<_>>() {
742            let preds_empty = abstract_graph
743                .predecessors
744                .get(&node_id)
745                .map(|v| v.is_empty())
746                .unwrap_or(true);
747            let succs_empty = abstract_graph
748                .successors
749                .get(&node_id)
750                .map(|v| v.is_empty())
751                .unwrap_or(true);
752            if preds_empty {
753                abstract_graph.entry_nodes.push(node_id);
754            }
755            if succs_empty {
756                abstract_graph.exit_nodes.push(node_id);
757            }
758        }
759        Ok(abstract_graph)
760    }
761
762    /// Map a concrete [`Operation`] to an [`AbstractNodeOp`].
763    ///
764    /// Operations without a precise abstract semantics in this framework
765    /// fall through to [`AbstractNodeOp::Unknown`], which the transfer
766    /// function maps to `domain.top()` — a sound over-approximation.
767    fn classify_operation(op: &Operation) -> AbstractNodeOp {
768        match op {
769            Operation::Input | Operation::Parameter(_) => AbstractNodeOp::Input,
770            Operation::Constant(info) => {
771                let v = match &info.value {
772                    OpConstantValue::Float(f) | OpConstantValue::Scalar(f) => Some(*f),
773                    OpConstantValue::Int(i) | OpConstantValue::IntScalar(i) => Some(*i as f64),
774                    OpConstantValue::UInt(u) => Some(*u as f64),
775                    OpConstantValue::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
776                    _ => None,
777                };
778                match v {
779                    Some(value) => AbstractNodeOp::Constant(value),
780                    None => AbstractNodeOp::Unknown,
781                }
782            }
783            Operation::Add => AbstractNodeOp::Binary(BinaryAbstractOp::Add),
784            Operation::Sub => AbstractNodeOp::Binary(BinaryAbstractOp::Sub),
785            Operation::Mul => AbstractNodeOp::Binary(BinaryAbstractOp::Mul),
786            Operation::Div => AbstractNodeOp::Binary(BinaryAbstractOp::Div),
787            Operation::Neg => AbstractNodeOp::Unary(UnaryAbstractOp::Neg),
788            Operation::Abs => AbstractNodeOp::Unary(UnaryAbstractOp::Abs),
789            Operation::Sqrt => AbstractNodeOp::Unary(UnaryAbstractOp::Sqrt),
790            Operation::Sin => AbstractNodeOp::Unary(UnaryAbstractOp::Sin),
791            Operation::Cos => AbstractNodeOp::Unary(UnaryAbstractOp::Cos),
792            Operation::Exp => AbstractNodeOp::Unary(UnaryAbstractOp::Exp),
793            Operation::Log => AbstractNodeOp::Unary(UnaryAbstractOp::Log),
794            _ => AbstractNodeOp::Unknown,
795        }
796    }
797
798    /// Compute the abstract post-state of `node` by applying its transfer
799    /// function over the current `state`.
800    ///
801    /// This is the abstract-semantics dispatcher used by both the
802    /// forward worklist and the per-node propagation logic. Missing
803    /// predecessor states default to `domain.bottom()`, which models
804    /// "not yet visited" without polluting the join.
805    fn transfer_node(
806        node: NodeId,
807        graph: &AbstractGraph,
808        state: &HashMap<NodeId, AbstractValue>,
809        domain: &dyn AbstractDomain,
810    ) -> JitResult<AbstractValue> {
811        let op = match graph.node_ops.get(&node) {
812            Some(op) => op,
813            None => return Ok(domain.top()),
814        };
815        match op {
816            AbstractNodeOp::Input | AbstractNodeOp::Unknown => Ok(domain.top()),
817            AbstractNodeOp::Constant(value) => domain.lift_constant(*value),
818            AbstractNodeOp::Binary(bin_op) => {
819                let preds = graph.predecessors_of(node);
820                if preds.len() < 2 {
821                    return Ok(domain.top());
822                }
823                let left = state
824                    .get(&preds[0])
825                    .cloned()
826                    .unwrap_or_else(|| domain.bottom());
827                let right = state
828                    .get(&preds[1])
829                    .cloned()
830                    .unwrap_or_else(|| domain.bottom());
831                domain.abstract_binary_op(*bin_op, &left, &right)
832            }
833            AbstractNodeOp::Unary(un_op) => {
834                let preds = graph.predecessors_of(node);
835                if preds.is_empty() {
836                    return Ok(domain.top());
837                }
838                let operand = state
839                    .get(&preds[0])
840                    .cloned()
841                    .unwrap_or_else(|| domain.bottom());
842                domain.abstract_unary_op(*un_op, &operand)
843            }
844        }
845    }
846
847    /// Test abstract-domain equality via the partial order in both
848    /// directions: `a == b ⇔ a ⊑ b ∧ b ⊑ a`.
849    ///
850    /// Used as the worklist convergence test because [`AbstractValue`]
851    /// does not derive [`PartialEq`] for all variants.
852    fn values_equal(a: &AbstractValue, b: &AbstractValue, domain: &dyn AbstractDomain) -> bool {
853        domain.less_equal(a, b) && domain.less_equal(b, a)
854    }
855
856    /// Forward Kildall worklist with widening delay.
857    ///
858    /// Algorithm:
859    /// 1. Initialize each node's pre-state to `domain.bottom()`.
860    /// 2. Push all entry nodes onto the worklist.
861    /// 3. Pop a node, compute its new post-state via [`Self::transfer_node`],
862    ///    join it with the previous post-state, optionally widen if the
863    ///    visit count exceeds `widening_delay`, and if the result differs,
864    ///    schedule all successors.
865    /// 4. Cap iterations at `config.max_iterations`; report convergence.
866    fn forward_analysis(
867        &mut self,
868        graph: &AbstractGraph,
869        domain: &dyn AbstractDomain,
870    ) -> JitResult<ForwardAnalysisResult> {
871        let mut result = ForwardAnalysisResult::new();
872        let mut post_states: HashMap<NodeId, AbstractValue> = HashMap::new();
873        let mut pre_states: HashMap<NodeId, AbstractValue> = HashMap::new();
874        let mut visit_count: HashMap<NodeId, usize> = HashMap::new();
875
876        for node in graph.nodes() {
877            post_states.insert(node, domain.bottom());
878            pre_states.insert(node, domain.bottom());
879        }
880
881        let mut worklist: VecDeque<NodeId> = if graph.entry_nodes.is_empty() {
882            graph.nodes().collect()
883        } else {
884            graph.entry_nodes.iter().copied().collect()
885        };
886        let mut in_worklist: HashSet<NodeId> = worklist.iter().copied().collect();
887
888        let mut iterations = 0usize;
889        let max_iterations = self.config.max_iterations.max(1);
890        let widening_delay = self.config.widening_delay;
891        let mut converged = false;
892
893        while let Some(node) = worklist.pop_front() {
894            in_worklist.remove(&node);
895            iterations += 1;
896            if iterations > max_iterations {
897                break;
898            }
899
900            // Pre-state = join of predecessors' post-states.
901            let pre = Self::join_predecessor_states(node, graph, &post_states, domain)?;
902            pre_states.insert(node, pre);
903
904            // Compute the new post-state via the transfer function.
905            let transferred = Self::transfer_node(node, graph, &post_states, domain)?;
906            let old_post = post_states
907                .get(&node)
908                .cloned()
909                .unwrap_or_else(|| domain.bottom());
910
911            let visits = visit_count.entry(node).or_insert(0);
912            *visits += 1;
913            let new_post = if widening_delay > 0 && *visits > widening_delay {
914                domain.widen(&old_post, &transferred)?
915            } else {
916                domain.join(&old_post, &transferred)?
917            };
918
919            if !Self::values_equal(&new_post, &old_post, domain) {
920                post_states.insert(node, new_post);
921                for succ in graph.successors_of(node) {
922                    if in_worklist.insert(*succ) {
923                        worklist.push_back(*succ);
924                    }
925                }
926            }
927
928            if worklist.is_empty() {
929                converged = true;
930                break;
931            }
932        }
933
934        // Optional narrowing pass to refine widened states.
935        if self.config.enable_narrowing {
936            for node in graph.nodes() {
937                let transferred = Self::transfer_node(node, graph, &post_states, domain)?;
938                let current = post_states
939                    .get(&node)
940                    .cloned()
941                    .unwrap_or_else(|| domain.bottom());
942                let narrowed = domain.narrow(&current, &transferred)?;
943                post_states.insert(node, narrowed);
944            }
945        }
946
947        result.pre_states = pre_states;
948        result.post_states = post_states;
949        result.iterations = iterations;
950        result.converged = converged && iterations <= max_iterations;
951        Ok(result)
952    }
953
954    /// Compute the join of the post-states of `node`'s predecessors.
955    ///
956    /// Used to derive a node's pre-state in forward analysis. Returns
957    /// `domain.bottom()` for entry nodes (no predecessors).
958    fn join_predecessor_states(
959        node: NodeId,
960        graph: &AbstractGraph,
961        post_states: &HashMap<NodeId, AbstractValue>,
962        domain: &dyn AbstractDomain,
963    ) -> JitResult<AbstractValue> {
964        let preds = graph.predecessors_of(node);
965        if preds.is_empty() {
966            return Ok(domain.bottom());
967        }
968        let mut acc = domain.bottom();
969        for pred in preds {
970            let pred_state = post_states
971                .get(pred)
972                .cloned()
973                .unwrap_or_else(|| domain.bottom());
974            acc = domain.join(&acc, &pred_state)?;
975        }
976        Ok(acc)
977    }
978
979    /// Backward analysis: same Kildall fixed-point structure as
980    /// [`Self::forward_analysis`] but propagating from exits to entries.
981    ///
982    /// For now this is a structural symmetric pass — each node's
983    /// post-state is the join of its successors' pre-states; the
984    /// transfer function is the identity (the framework does not yet
985    /// model precise backward semantics for arithmetic ops). This is a
986    /// sound under-approximation of usable backward information.
987    fn backward_analysis(
988        &mut self,
989        graph: &AbstractGraph,
990        domain: &dyn AbstractDomain,
991    ) -> JitResult<BackwardAnalysisResult> {
992        let mut result = BackwardAnalysisResult::new();
993        let mut pre_states: HashMap<NodeId, AbstractValue> = HashMap::new();
994        let mut post_states: HashMap<NodeId, AbstractValue> = HashMap::new();
995        let mut visit_count: HashMap<NodeId, usize> = HashMap::new();
996
997        for node in graph.nodes() {
998            pre_states.insert(node, domain.bottom());
999            post_states.insert(node, domain.top());
1000        }
1001
1002        let mut worklist: VecDeque<NodeId> = if graph.exit_nodes.is_empty() {
1003            graph.nodes().collect()
1004        } else {
1005            graph.exit_nodes.iter().copied().collect()
1006        };
1007        let mut in_worklist: HashSet<NodeId> = worklist.iter().copied().collect();
1008
1009        let mut iterations = 0usize;
1010        let max_iterations = self.config.max_iterations.max(1);
1011        let widening_delay = self.config.widening_delay;
1012        let mut converged = false;
1013
1014        while let Some(node) = worklist.pop_front() {
1015            in_worklist.remove(&node);
1016            iterations += 1;
1017            if iterations > max_iterations {
1018                break;
1019            }
1020
1021            // Post-state = join of successors' pre-states.
1022            let succs = graph.successors_of(node);
1023            let post = if succs.is_empty() {
1024                domain.top()
1025            } else {
1026                let mut acc = domain.bottom();
1027                for s in succs {
1028                    let s_state = pre_states
1029                        .get(s)
1030                        .cloned()
1031                        .unwrap_or_else(|| domain.bottom());
1032                    acc = domain.join(&acc, &s_state)?;
1033                }
1034                acc
1035            };
1036            post_states.insert(node, post.clone());
1037
1038            // Backward transfer: identity (sound).
1039            let old_pre = pre_states
1040                .get(&node)
1041                .cloned()
1042                .unwrap_or_else(|| domain.bottom());
1043            let visits = visit_count.entry(node).or_insert(0);
1044            *visits += 1;
1045            let new_pre = if widening_delay > 0 && *visits > widening_delay {
1046                domain.widen(&old_pre, &post)?
1047            } else {
1048                domain.join(&old_pre, &post)?
1049            };
1050
1051            if !Self::values_equal(&new_pre, &old_pre, domain) {
1052                pre_states.insert(node, new_pre);
1053                for pred in graph.predecessors_of(node) {
1054                    if in_worklist.insert(*pred) {
1055                        worklist.push_back(*pred);
1056                    }
1057                }
1058            }
1059
1060            if worklist.is_empty() {
1061                converged = true;
1062                break;
1063            }
1064        }
1065
1066        result.pre_states = pre_states;
1067        result.post_states = post_states;
1068        result.iterations = iterations;
1069        result.converged = converged && iterations <= max_iterations;
1070        Ok(result)
1071    }
1072
1073    /// Score the precision of a forward analysis result.
1074    ///
1075    /// Per-node precision is taken from [`AbstractValue::precision`].
1076    /// The overall precision is the unweighted average over all nodes.
1077    /// Nodes whose precision is below `config.precision_threshold` are
1078    /// reported as improvement opportunities.
1079    fn analyze_precision(
1080        &self,
1081        forward_result: &ForwardAnalysisResult,
1082        graph: &AbstractGraph,
1083    ) -> PrecisionAnalysis {
1084        let mut node_precision = HashMap::new();
1085        let mut total = 0.0f64;
1086        let mut count = 0usize;
1087        let mut suggestions = Vec::new();
1088
1089        for node in graph.nodes() {
1090            let val = forward_result.post_states.get(&node);
1091            let p = val.map(|v| v.precision()).unwrap_or(0.0);
1092            node_precision.insert(node, p);
1093            total += p;
1094            count += 1;
1095            if p < self.config.precision_threshold {
1096                suggestions.push(format!(
1097                    "node {:?}: low precision ({:.2}); consider richer abstract domain",
1098                    node, p
1099                ));
1100            }
1101        }
1102        let overall_precision = if count == 0 {
1103            0.0
1104        } else {
1105            total / count as f64
1106        };
1107        PrecisionAnalysis {
1108            overall_precision,
1109            node_precision,
1110            improvement_suggestions: suggestions,
1111        }
1112    }
1113
1114    /// Heuristic performance analysis driven by node operation
1115    /// categories and abstract states.
1116    ///
1117    /// Bottlenecks are flagged when a node is computation-intensive
1118    /// (matrix multiply, convolution, reductions) or when its abstract
1119    /// value is fully unknown (e.g. division by an interval containing
1120    /// zero), and optimization opportunities surface for nodes with
1121    /// fully-determined abstract values (constant folding).
1122    fn analyze_performance(
1123        &self,
1124        forward_result: &ForwardAnalysisResult,
1125        graph: &ComputationGraph,
1126    ) -> PerformanceAnalysis {
1127        let mut bottlenecks = Vec::new();
1128        let mut opportunities = Vec::new();
1129        let mut weight = 0.0f64;
1130        let total_nodes = graph.node_count().max(1) as f64;
1131
1132        for (node_id, node) in graph.nodes() {
1133            let complexity = node.complexity_estimate() as f64;
1134            weight += complexity;
1135            let category = node.operation_category();
1136            if matches!(
1137                category,
1138                crate::graph::core::OperationCategory::LinearAlgebra
1139                    | crate::graph::core::OperationCategory::NeuralNetwork
1140                    | crate::graph::core::OperationCategory::Reduction
1141            ) {
1142                bottlenecks.push(PerformanceBottleneck {
1143                    bottleneck_type: BottleneckType::ComputationIntensive,
1144                    location: node_id,
1145                    severity: (complexity / 1000.0).min(1.0),
1146                    description: format!("{:?} is computation-intensive", node.operation_type()),
1147                });
1148            }
1149
1150            if let Some(value) = forward_result.post_states.get(&node_id) {
1151                if value.is_constant() {
1152                    opportunities.push(OptimizationOpportunity {
1153                        optimization_type: OptimizationType::ConstantFolding,
1154                        location: node_id,
1155                        benefit: 0.9,
1156                        description: "abstract value is a constant; fold at compile time"
1157                            .to_string(),
1158                    });
1159                }
1160            }
1161        }
1162
1163        let complexity_score = (weight / (total_nodes * 100.0)).min(1.0);
1164        PerformanceAnalysis {
1165            complexity_score,
1166            bottlenecks,
1167            optimization_opportunities: opportunities,
1168        }
1169    }
1170
1171    /// Analyze a single basic block as a straight-line program.
1172    ///
1173    /// Each instruction is processed in order; we maintain a map from
1174    /// IR values to abstract values, and compute entry/exit states as
1175    /// the join over all known values at the corresponding boundary.
1176    /// Convergence is trivial for straight-line code (one pass).
1177    fn analyze_block(&mut self, block: &BasicBlock) -> JitResult<AbstractFunctionResult> {
1178        let domain = self.domain_factory.create_domain(&self.config.domain_type);
1179        let mut value_state: HashMap<IrValue, AbstractValue> = HashMap::new();
1180
1181        // Block params seed at top (unknown a priori).
1182        for p in &block.params {
1183            value_state.insert(*p, domain.top());
1184        }
1185
1186        let mut iterations = 0usize;
1187        for instr in &block.instructions {
1188            iterations += 1;
1189            let abstract_value = Self::transfer_instruction(instr, &value_state, domain.as_ref())?;
1190            if let Some(result) = instr.result {
1191                value_state.insert(result, abstract_value);
1192            }
1193        }
1194
1195        let entry_state = Self::join_value_state(&value_state, domain.as_ref(), true)?;
1196        let exit_state = Self::join_value_state(&value_state, domain.as_ref(), false)?;
1197
1198        let invariants = Self::block_invariants(block, &value_state);
1199
1200        let forward_result = FunctionForwardResult {
1201            converged: true,
1202            iterations,
1203            entry_state: Some(entry_state),
1204            exit_state: Some(exit_state.clone()),
1205        };
1206
1207        let backward_result = if self.config.enable_backward_analysis {
1208            Some(FunctionBackwardResult {
1209                converged: true,
1210                iterations,
1211                entry_state: Some(domain.top()),
1212                exit_state: Some(exit_state),
1213            })
1214        } else {
1215            None
1216        };
1217
1218        Ok(AbstractFunctionResult {
1219            forward_result,
1220            backward_result,
1221            invariants,
1222        })
1223    }
1224
1225    /// Apply the abstract semantics of a single IR instruction.
1226    ///
1227    /// Operands are looked up in `state`; missing operands fall back to
1228    /// `domain.top()`. Constants are lifted via [`AbstractDomain::lift_constant`];
1229    /// arithmetic opcodes dispatch to [`AbstractDomain::abstract_binary_op`]
1230    /// and [`AbstractDomain::abstract_unary_op`]; everything else is
1231    /// approximated as `top` (sound).
1232    fn transfer_instruction(
1233        instr: &crate::ir::Instruction,
1234        state: &HashMap<IrValue, AbstractValue>,
1235        domain: &dyn AbstractDomain,
1236    ) -> JitResult<AbstractValue> {
1237        let lookup = |v: &IrValue| -> AbstractValue {
1238            state.get(v).cloned().unwrap_or_else(|| domain.top())
1239        };
1240
1241        match &instr.opcode {
1242            IrOpcode::Const => match instr.attrs.get("value") {
1243                Some(crate::ir::IrAttribute::Float(value)) => domain.lift_constant(*value),
1244                Some(crate::ir::IrAttribute::Int(value)) => domain.lift_constant(*value as f64),
1245                Some(crate::ir::IrAttribute::Bool(value)) => {
1246                    domain.lift_constant(if *value { 1.0 } else { 0.0 })
1247                }
1248                _ => Ok(domain.top()),
1249            },
1250            IrOpcode::Add | IrOpcode::Sub | IrOpcode::Mul | IrOpcode::Div => {
1251                if instr.operands.len() < 2 {
1252                    return Ok(domain.top());
1253                }
1254                let l = lookup(&instr.operands[0]);
1255                let r = lookup(&instr.operands[1]);
1256                let op = match instr.opcode {
1257                    IrOpcode::Add => BinaryAbstractOp::Add,
1258                    IrOpcode::Sub => BinaryAbstractOp::Sub,
1259                    IrOpcode::Mul => BinaryAbstractOp::Mul,
1260                    IrOpcode::Div => BinaryAbstractOp::Div,
1261                    _ => unreachable!("matched outer opcode"),
1262                };
1263                domain.abstract_binary_op(op, &l, &r)
1264            }
1265            IrOpcode::Neg
1266            | IrOpcode::Abs
1267            | IrOpcode::Sqrt
1268            | IrOpcode::Sin
1269            | IrOpcode::Cos
1270            | IrOpcode::Exp
1271            | IrOpcode::Log => {
1272                if instr.operands.is_empty() {
1273                    return Ok(domain.top());
1274                }
1275                let operand = lookup(&instr.operands[0]);
1276                let op = match instr.opcode {
1277                    IrOpcode::Neg => UnaryAbstractOp::Neg,
1278                    IrOpcode::Abs => UnaryAbstractOp::Abs,
1279                    IrOpcode::Sqrt => UnaryAbstractOp::Sqrt,
1280                    IrOpcode::Sin => UnaryAbstractOp::Sin,
1281                    IrOpcode::Cos => UnaryAbstractOp::Cos,
1282                    IrOpcode::Exp => UnaryAbstractOp::Exp,
1283                    IrOpcode::Log => UnaryAbstractOp::Log,
1284                    _ => unreachable!("matched outer opcode"),
1285                };
1286                domain.abstract_unary_op(op, &operand)
1287            }
1288            _ => Ok(domain.top()),
1289        }
1290    }
1291
1292    /// Compute the join (or initial value) over a value-state map.
1293    ///
1294    /// `is_entry == true` returns `domain.top()` (no constraints at entry).
1295    /// `is_entry == false` returns the join of all values, modeling the
1296    /// post-state of the block.
1297    fn join_value_state(
1298        state: &HashMap<IrValue, AbstractValue>,
1299        domain: &dyn AbstractDomain,
1300        is_entry: bool,
1301    ) -> JitResult<AbstractValue> {
1302        if is_entry || state.is_empty() {
1303            return Ok(domain.top());
1304        }
1305        let mut acc = domain.bottom();
1306        for v in state.values() {
1307            acc = domain.join(&acc, v)?;
1308        }
1309        Ok(acc)
1310    }
1311
1312    /// Detect block-level invariants (constant values).
1313    ///
1314    /// For each value whose abstract state is a singleton (interval with
1315    /// `min == max`, sign `Zero`, or constant `Value`), report a
1316    /// numerical-property invariant.
1317    fn block_invariants(
1318        block: &BasicBlock,
1319        state: &HashMap<IrValue, AbstractValue>,
1320    ) -> Vec<FunctionInvariant> {
1321        let mut invariants = Vec::new();
1322        for (value, av) in state {
1323            if av.is_constant() {
1324                invariants.push(FunctionInvariant {
1325                    invariant_type: InvariantType::NumericalProperty,
1326                    description: format!("value {:?} is constant", value),
1327                    confidence: 1.0,
1328                    location: format!("block {}", block.id),
1329                });
1330            }
1331        }
1332        invariants
1333    }
1334
1335    /// Build the per-function call graph + global invariants from
1336    /// per-block analysis results.
1337    ///
1338    /// At the IR-module level used here, blocks are not separable
1339    /// functions, so the call graph is a single synthetic entry "module"
1340    /// pointing to all analyzed blocks; global invariants are the union
1341    /// of block invariants whose confidence is high.
1342    fn interprocedural_analysis_blocks(
1343        &self,
1344        block_results: &HashMap<BlockId, AbstractFunctionResult>,
1345    ) -> JitResult<InterproceduralAnalysisResult> {
1346        let mut call_graph: HashMap<String, Vec<String>> = HashMap::new();
1347        let mut global_invariants = Vec::new();
1348        let block_names: Vec<String> = block_results
1349            .keys()
1350            .map(|id| format!("block_{}", id))
1351            .collect();
1352        call_graph.insert("module".to_string(), block_names);
1353
1354        for (block_id, res) in block_results {
1355            for inv in &res.invariants {
1356                if inv.confidence >= self.config.precision_threshold {
1357                    global_invariants.push(Invariant {
1358                        invariant_type: inv.invariant_type.clone(),
1359                        description: format!("block {}: {}", block_id, inv.description),
1360                        confidence: inv.confidence,
1361                        location: inv.location.clone(),
1362                    });
1363                }
1364            }
1365        }
1366        Ok(InterproceduralAnalysisResult {
1367            call_graph,
1368            global_invariants,
1369        })
1370    }
1371
1372    /// Lift per-block invariants to module scope.
1373    ///
1374    /// A module-level invariant is reported whenever the same kind of
1375    /// per-block invariant holds in *every* analyzed block — e.g. all
1376    /// blocks return constants, in which case the module is functionally
1377    /// constant.
1378    fn detect_module_invariants_blocks(
1379        &self,
1380        block_results: &HashMap<BlockId, AbstractFunctionResult>,
1381    ) -> JitResult<Vec<ModuleInvariant>> {
1382        if block_results.is_empty() {
1383            return Ok(Vec::new());
1384        }
1385        let mut all_constant = true;
1386        for res in block_results.values() {
1387            let is_block_constant = res
1388                .forward_result
1389                .exit_state
1390                .as_ref()
1391                .map(AbstractValue::is_constant)
1392                .unwrap_or(false);
1393            if !is_block_constant {
1394                all_constant = false;
1395                break;
1396            }
1397        }
1398        let mut invariants = Vec::new();
1399        if all_constant {
1400            invariants.push(ModuleInvariant {
1401                invariant_type: InvariantType::NumericalProperty,
1402                description: "all blocks have constant exit state".to_string(),
1403                confidence: 1.0,
1404                scope: "module".to_string(),
1405            });
1406        }
1407        Ok(invariants)
1408    }
1409}
1410
1411/// Abstract operation kind associated with a node, used to dispatch
1412/// transfer functions over an [`AbstractDomain`].
1413///
1414/// This enum decouples the high-level [`Operation`] vocabulary from
1415/// the abstract semantics, keeping the analysis simple and total: any
1416/// concrete op that does not have a known abstract semantics is mapped
1417/// to [`AbstractNodeOp::Unknown`] and conservatively transferred to
1418/// [`AbstractDomain::top`].
1419#[derive(Debug, Clone)]
1420pub enum AbstractNodeOp {
1421    /// Input or parameter node — its abstract value is the domain top
1422    /// (no precondition known a priori).
1423    Input,
1424    /// A constant node whose concrete value is `value`.
1425    Constant(f64),
1426    /// A binary arithmetic/comparison operation.
1427    Binary(BinaryAbstractOp),
1428    /// A unary arithmetic/intrinsic operation.
1429    Unary(UnaryAbstractOp),
1430    /// Operation with no precise abstract semantics in this framework;
1431    /// always transferred to top (sound over-approximation).
1432    Unknown,
1433}
1434
1435/// Abstract representation of a [`ComputationGraph`] used by the
1436/// abstract interpreter.
1437///
1438/// The structure stores per-node operation kinds plus pre-computed
1439/// successor/predecessor adjacency in topological-friendly form so the
1440/// Kildall fixed-point worklist can iterate without paying for repeated
1441/// graph queries.
1442#[derive(Debug, Clone, Default)]
1443pub struct AbstractGraph {
1444    /// Operation kind for each node.
1445    pub node_ops: HashMap<NodeId, AbstractNodeOp>,
1446    /// Operands (predecessors) of each node, in input order.
1447    pub predecessors: HashMap<NodeId, Vec<NodeId>>,
1448    /// Successors of each node.
1449    pub successors: HashMap<NodeId, Vec<NodeId>>,
1450    /// Nodes with no predecessors (entries to the analysis).
1451    pub entry_nodes: Vec<NodeId>,
1452    /// Nodes with no successors (analysis sinks).
1453    pub exit_nodes: Vec<NodeId>,
1454}
1455
1456impl AbstractGraph {
1457    /// Create an empty abstract graph.
1458    pub fn new() -> Self {
1459        Self::default()
1460    }
1461
1462    /// Iterate over all nodes in the graph.
1463    pub fn nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
1464        self.node_ops.keys().copied()
1465    }
1466
1467    /// Number of nodes in the graph.
1468    pub fn node_count(&self) -> usize {
1469        self.node_ops.len()
1470    }
1471
1472    /// Successors of `node` (empty slice if none).
1473    pub fn successors_of(&self, node: NodeId) -> &[NodeId] {
1474        self.successors.get(&node).map(Vec::as_slice).unwrap_or(&[])
1475    }
1476
1477    /// Predecessors of `node` (empty slice if none).
1478    pub fn predecessors_of(&self, node: NodeId) -> &[NodeId] {
1479        self.predecessors
1480            .get(&node)
1481            .map(Vec::as_slice)
1482            .unwrap_or(&[])
1483    }
1484}