Skip to main content

omena_abstract_value/
flow.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use omena_incremental::{
4    IncrementalGraphInputV0, IncrementalNodeInputV0, IncrementalRevisionV0, IncrementalSnapshotV0,
5    OmenaIncrementalDatabaseV0, plan_incremental_computation, snapshot_from_graph_input,
6};
7
8use crate::*;
9
10pub fn summarize_omena_abstract_value_flow_analysis() -> AbstractValueFlowAnalysisSummaryV0 {
11    AbstractValueFlowAnalysisSummaryV0 {
12        schema_version: "0",
13        product: "omena-abstract-value.flow-analysis",
14        context_sensitivity: "1-cfa",
15        incremental_engine: "omena-incremental",
16        analysis_scopes: vec![
17            "singleContext",
18            "multiContextBatch",
19            "callSiteBatch",
20            "zeroCfaCallSiteBatch",
21            "kLimitedCallSiteBatch",
22            "controlFlowGraph",
23        ],
24        reuse_policy: "reuse previous context analysis when its omena-incremental plan is clean",
25        transfer_kinds: vec!["assignFacts", "refineFacts", "concatFacts", "join"],
26        max_iterations: MAX_FLOW_ANALYSIS_ITERATIONS,
27    }
28}
29
30pub fn analyze_class_value_flow(graph: &ClassValueFlowGraphV0) -> ClassValueFlowAnalysisV0 {
31    let mut values = graph
32        .nodes
33        .iter()
34        .map(|node| (node.id.clone(), bottom_class_value()))
35        .collect::<BTreeMap<_, _>>();
36    let mut converged = false;
37    let mut iteration_count = 0;
38
39    for iteration in 1..=MAX_FLOW_ANALYSIS_ITERATIONS {
40        iteration_count = iteration;
41        let mut changed = false;
42
43        for node in &graph.nodes {
44            let incoming = join_predecessor_flow_values(node, &values);
45            let next = apply_flow_transfer(&incoming, &node.transfer);
46
47            if values.get(&node.id) != Some(&next) {
48                values.insert(node.id.clone(), next);
49                changed = true;
50            }
51        }
52
53        if !changed {
54            converged = true;
55            break;
56        }
57    }
58
59    ClassValueFlowAnalysisV0 {
60        schema_version: "0",
61        product: "omena-abstract-value.flow-analysis",
62        context_sensitivity: "1-cfa",
63        context_key: graph.context_key.clone(),
64        converged,
65        iteration_count,
66        nodes: graph
67            .nodes
68            .iter()
69            .map(|node| {
70                let value = values
71                    .get(&node.id)
72                    .cloned()
73                    .unwrap_or_else(bottom_class_value);
74                ClassValueFlowNodeResultV0 {
75                    id: node.id.clone(),
76                    predecessor_ids: node.predecessors.clone(),
77                    transfer_kind: flow_transfer_kind(&node.transfer),
78                    value_kind: abstract_class_value_kind(&value),
79                    value,
80                }
81            })
82            .collect(),
83    }
84}
85
86pub fn analyze_class_value_control_flow_graph(
87    graph: &ClassValueControlFlowGraphV0,
88) -> ClassValueControlFlowAnalysisV0 {
89    let reachable_block_ids = reachable_control_flow_block_ids(graph);
90    let reachable_node_ids = graph
91        .blocks
92        .iter()
93        .filter(|block| reachable_block_ids.contains(&block.id))
94        .flat_map(|block| block.nodes.iter().map(|node| node.id.clone()))
95        .collect::<BTreeSet<_>>();
96    let flow_graph = ClassValueFlowGraphV0 {
97        context_key: graph.context_key.clone(),
98        nodes: graph
99            .blocks
100            .iter()
101            .filter(|block| reachable_block_ids.contains(&block.id))
102            .flat_map(|block| {
103                block.nodes.iter().map(|node| ClassValueFlowNodeV0 {
104                    id: node.id.clone(),
105                    predecessors: node
106                        .predecessors
107                        .iter()
108                        .filter(|id| reachable_node_ids.contains(id.as_str()))
109                        .cloned()
110                        .collect(),
111                    transfer: node.transfer.clone(),
112                })
113            })
114            .collect(),
115    };
116    let flow_analysis = analyze_class_value_flow(&flow_graph);
117    let unreachable_block_ids = graph
118        .blocks
119        .iter()
120        .filter(|block| !reachable_block_ids.contains(&block.id))
121        .map(|block| block.id.clone())
122        .collect::<Vec<_>>();
123    let branch_block_ids = graph
124        .blocks
125        .iter()
126        .filter(|block| block.successor_block_ids.len() > 1)
127        .map(|block| block.id.clone())
128        .collect::<Vec<_>>();
129    let predecessor_counts = control_flow_predecessor_counts(graph);
130    let join_block_ids = graph
131        .blocks
132        .iter()
133        .filter(|block| predecessor_counts.get(&block.id).copied().unwrap_or(0) > 1)
134        .map(|block| block.id.clone())
135        .collect::<Vec<_>>();
136    let blocks = graph
137        .blocks
138        .iter()
139        .map(|block| {
140            let reachable = reachable_block_ids.contains(&block.id);
141            let exit_value = if reachable {
142                block
143                    .nodes
144                    .iter()
145                    .rev()
146                    .find_map(|node| flow_analysis_node_value(&flow_analysis, &node.id))
147                    .cloned()
148                    .unwrap_or_else(bottom_class_value)
149            } else {
150                bottom_class_value()
151            };
152
153            ClassValueControlFlowBlockResultV0 {
154                block_id: block.id.clone(),
155                reachable,
156                node_ids: block.nodes.iter().map(|node| node.id.clone()).collect(),
157                successor_block_ids: block.successor_block_ids.clone(),
158                exit_value_kind: abstract_class_value_kind(&exit_value),
159                exit_value,
160            }
161        })
162        .collect::<Vec<_>>();
163
164    ClassValueControlFlowAnalysisV0 {
165        schema_version: "0",
166        product: "omena-abstract-value.control-flow-analysis",
167        context_sensitivity: "1-cfa",
168        context_key: graph.context_key.clone(),
169        block_count: graph.blocks.len(),
170        edge_count: graph
171            .blocks
172            .iter()
173            .map(|block| block.successor_block_ids.len())
174            .sum(),
175        reachable_block_count: reachable_block_ids.len(),
176        unreachable_block_ids,
177        branch_block_ids,
178        join_block_ids,
179        flow_analysis,
180        blocks,
181    }
182}
183
184pub fn analyze_class_value_flow_incremental(
185    graph: &ClassValueFlowGraphV0,
186    previous_snapshot: Option<&IncrementalSnapshotV0>,
187    revision: u64,
188) -> ClassValueFlowIncrementalAnalysisV0 {
189    analyze_class_value_flow_incremental_with_reuse(graph, previous_snapshot, None, revision)
190}
191
192pub fn analyze_class_value_flow_incremental_with_reuse(
193    graph: &ClassValueFlowGraphV0,
194    previous_snapshot: Option<&IncrementalSnapshotV0>,
195    previous_analysis: Option<&ClassValueFlowAnalysisV0>,
196    revision: u64,
197) -> ClassValueFlowIncrementalAnalysisV0 {
198    let incremental_input = class_value_flow_incremental_input(graph, revision);
199    let incremental_plan = plan_incremental_computation(&incremental_input, previous_snapshot);
200    let next_snapshot = snapshot_from_graph_input(&incremental_input);
201    let reused_previous_analysis =
202        incremental_plan.dirty_node_count == 0 && previous_analysis.is_some();
203    let analysis = match (incremental_plan.dirty_node_count, previous_analysis) {
204        (0, Some(previous_analysis)) => previous_analysis.clone(),
205        _ => analyze_class_value_flow(graph),
206    };
207
208    ClassValueFlowIncrementalAnalysisV0 {
209        schema_version: "0",
210        product: "omena-abstract-value.incremental-flow-analysis",
211        reused_previous_analysis,
212        incremental_plan,
213        next_snapshot,
214        analysis,
215    }
216}
217
218pub fn analyze_class_value_flow_incremental_with_database(
219    graph: &ClassValueFlowGraphV0,
220    incremental_database: &mut OmenaIncrementalDatabaseV0,
221    previous_analysis: Option<&ClassValueFlowAnalysisV0>,
222    revision: u64,
223) -> ClassValueFlowIncrementalAnalysisV0 {
224    let incremental_input = class_value_flow_incremental_input(graph, revision);
225    let update = incremental_database.plan_and_upsert_graph_input(&incremental_input);
226    let reused_previous_analysis =
227        update.incremental_plan.dirty_node_count == 0 && previous_analysis.is_some();
228    let analysis = match (update.incremental_plan.dirty_node_count, previous_analysis) {
229        (0, Some(previous_analysis)) => previous_analysis.clone(),
230        _ => analyze_class_value_flow(graph),
231    };
232
233    ClassValueFlowIncrementalAnalysisV0 {
234        schema_version: "0",
235        product: "omena-abstract-value.incremental-flow-analysis",
236        reused_previous_analysis,
237        incremental_plan: update.incremental_plan,
238        next_snapshot: update.next_snapshot,
239        analysis,
240    }
241}
242
243pub fn analyze_class_value_flow_incremental_batch_with_reuse(
244    graphs: &[ClassValueFlowGraphV0],
245    previous_snapshots: &BTreeMap<String, IncrementalSnapshotV0>,
246    previous_analyses: &BTreeMap<String, ClassValueFlowAnalysisV0>,
247    revision: u64,
248) -> ClassValueFlowIncrementalBatchAnalysisV0 {
249    let entries = graphs
250        .iter()
251        .enumerate()
252        .map(|(index, graph)| {
253            let context_key = flow_graph_batch_context_key(graph, index);
254            let analysis = analyze_class_value_flow_incremental_with_reuse(
255                graph,
256                previous_snapshots.get(context_key.as_str()),
257                previous_analyses.get(context_key.as_str()),
258                revision,
259            );
260            ClassValueFlowIncrementalBatchEntryV0 {
261                context_key,
262                analysis,
263            }
264        })
265        .collect::<Vec<_>>();
266    let reused_context_count = entries
267        .iter()
268        .filter(|entry| entry.analysis.reused_previous_analysis)
269        .count();
270    let dirty_context_count = entries
271        .iter()
272        .filter(|entry| entry.analysis.incremental_plan.dirty_node_count > 0)
273        .count();
274
275    ClassValueFlowIncrementalBatchAnalysisV0 {
276        schema_version: "0",
277        product: "omena-abstract-value.incremental-flow-analysis-batch",
278        revision,
279        context_count: entries.len(),
280        dirty_context_count,
281        reused_context_count,
282        entries,
283    }
284}
285
286pub fn analyze_one_cfa_call_site_flows(
287    inputs: &[OneCfaCallSiteFlowInputV0],
288) -> OneCfaCallSiteFlowAnalysisV0 {
289    let entries = inputs
290        .iter()
291        .map(|input| {
292            let context_key = one_cfa_context_key(input);
293            let mut graph = input.graph.clone();
294            graph.context_key = Some(context_key.clone());
295            let analysis = analyze_class_value_flow(&graph);
296            let exit_value = flow_analysis_node_value(&analysis, &input.exit_node_id)
297                .cloned()
298                .unwrap_or_else(bottom_class_value);
299            let exit_value_kind = abstract_class_value_kind(&exit_value);
300
301            OneCfaCallSiteFlowEntryV0 {
302                callee_key: input.callee_key.clone(),
303                call_site_id: input.call_site_id.clone(),
304                context_key: context_key.clone(),
305                exit_node_id: input.exit_node_id.clone(),
306                exit_value_kind,
307                exit_value: exit_value.clone(),
308                analysis,
309                derivation: one_cfa_call_site_derivation(input, &context_key, &exit_value),
310            }
311        })
312        .collect::<Vec<_>>();
313    let callee_summaries = summarize_one_cfa_callees(&entries);
314
315    OneCfaCallSiteFlowAnalysisV0 {
316        schema_version: "0",
317        product: "omena-abstract-value.one-cfa-call-site-flow",
318        context_sensitivity: "1-cfa",
319        call_site_count: entries.len(),
320        callee_count: callee_summaries.len(),
321        entries,
322        callee_summaries,
323    }
324}
325
326pub fn analyze_k_limited_call_site_flows(
327    inputs: &[KLimitedCallSiteFlowInputV0],
328    max_context_depth: usize,
329) -> KLimitedCallSiteFlowAnalysisV0 {
330    let mut entries = inputs
331        .iter()
332        .map(|input| {
333            let context_key = k_limited_context_key(input, max_context_depth);
334            let mut graph = input.graph.clone();
335            graph.context_key = Some(context_key.clone());
336            let analysis = analyze_class_value_flow(&graph);
337            let exit_value = flow_analysis_node_value(&analysis, &input.exit_node_id)
338                .cloned()
339                .unwrap_or_else(bottom_class_value);
340            let exit_value_kind = abstract_class_value_kind(&exit_value);
341
342            KLimitedCallSiteFlowEntryV0 {
343                callee_key: input.callee_key.clone(),
344                call_site_stack: input.call_site_stack.clone(),
345                context_key,
346                exit_node_id: input.exit_node_id.clone(),
347                exit_value_kind,
348                exit_value,
349                analysis,
350            }
351        })
352        .collect::<Vec<_>>();
353    let joined_exit_values_by_context = entries.iter().fold(
354        BTreeMap::<String, AbstractClassValueV0>::new(),
355        |mut by_context, entry| {
356            by_context
357                .entry(entry.context_key.clone())
358                .and_modify(|value| {
359                    *value = join_abstract_class_values(value, &entry.exit_value);
360                })
361                .or_insert_with(|| entry.exit_value.clone());
362            by_context
363        },
364    );
365    for entry in &mut entries {
366        if let Some(joined_exit_value) = joined_exit_values_by_context.get(&entry.context_key) {
367            entry.exit_value = joined_exit_value.clone();
368            entry.exit_value_kind = abstract_class_value_kind(&entry.exit_value);
369        }
370    }
371    let callee_summaries = summarize_k_limited_callees(&entries);
372
373    KLimitedCallSiteFlowAnalysisV0 {
374        schema_version: "0",
375        product: "omena-abstract-value.k-limited-call-site-flow",
376        context_sensitivity: format!("{max_context_depth}-cfa"),
377        max_context_depth,
378        call_site_count: entries.len(),
379        callee_count: callee_summaries.len(),
380        entries,
381        callee_summaries,
382    }
383}
384
385pub fn class_value_flow_incremental_input(
386    graph: &ClassValueFlowGraphV0,
387    revision: u64,
388) -> IncrementalGraphInputV0 {
389    IncrementalGraphInputV0 {
390        revision: IncrementalRevisionV0 { value: revision },
391        nodes: graph
392            .nodes
393            .iter()
394            .map(|node| IncrementalNodeInputV0 {
395                id: node.id.clone(),
396                digest: flow_node_incremental_digest(node),
397                dependency_ids: node.predecessors.clone(),
398            })
399            .collect(),
400    }
401}
402
403fn flow_graph_batch_context_key(graph: &ClassValueFlowGraphV0, index: usize) -> String {
404    graph
405        .context_key
406        .clone()
407        .unwrap_or_else(|| format!("anonymous-context-{index}"))
408}
409
410fn one_cfa_context_key(input: &OneCfaCallSiteFlowInputV0) -> String {
411    format!("{}@{}", input.callee_key, input.call_site_id)
412}
413
414fn k_limited_context_key(input: &KLimitedCallSiteFlowInputV0, max_context_depth: usize) -> String {
415    let retained_stack = input
416        .call_site_stack
417        .iter()
418        .rev()
419        .take(max_context_depth)
420        .cloned()
421        .collect::<Vec<_>>()
422        .into_iter()
423        .rev()
424        .collect::<Vec<_>>();
425    let stack = if retained_stack.is_empty() {
426        "<root>".to_string()
427    } else {
428        retained_stack.join(" > ")
429    };
430
431    format!("{}@{}", input.callee_key, stack)
432}
433
434fn reachable_control_flow_block_ids(graph: &ClassValueControlFlowGraphV0) -> BTreeSet<String> {
435    let blocks_by_id = graph
436        .blocks
437        .iter()
438        .map(|block| (block.id.as_str(), block))
439        .collect::<BTreeMap<_, _>>();
440    let mut reachable = BTreeSet::new();
441    let mut worklist = vec![graph.entry_block_id.clone()];
442
443    while let Some(block_id) = worklist.pop() {
444        if !reachable.insert(block_id.clone()) {
445            continue;
446        }
447        let Some(block) = blocks_by_id.get(block_id.as_str()) else {
448            continue;
449        };
450        worklist.extend(block.successor_block_ids.iter().cloned());
451    }
452
453    reachable
454}
455
456fn control_flow_predecessor_counts(
457    graph: &ClassValueControlFlowGraphV0,
458) -> BTreeMap<String, usize> {
459    let mut counts = graph
460        .blocks
461        .iter()
462        .map(|block| (block.id.clone(), 0usize))
463        .collect::<BTreeMap<_, _>>();
464
465    for block in &graph.blocks {
466        for successor_id in &block.successor_block_ids {
467            *counts.entry(successor_id.clone()).or_default() += 1;
468        }
469    }
470
471    counts
472}
473
474fn flow_analysis_node_value<'a>(
475    analysis: &'a ClassValueFlowAnalysisV0,
476    node_id: &str,
477) -> Option<&'a AbstractClassValueV0> {
478    analysis
479        .nodes
480        .iter()
481        .find(|node| node.id == node_id)
482        .map(|node| &node.value)
483}
484
485fn one_cfa_call_site_derivation(
486    input: &OneCfaCallSiteFlowInputV0,
487    context_key: &str,
488    exit_value: &AbstractClassValueV0,
489) -> OneCfaCallSiteDerivationV0 {
490    OneCfaCallSiteDerivationV0 {
491        schema_version: "0",
492        product: "omena-abstract-value.one-cfa-call-site-derivation",
493        call_site_id: input.call_site_id.clone(),
494        context_key: context_key.to_string(),
495        steps: vec![
496            OneCfaCallSiteDerivationStepV0 {
497                operation: "contextFromCallSite",
498                result_kind: "context",
499                reason: "1-CFA separates flow facts by the immediate call-site identity",
500            },
501            OneCfaCallSiteDerivationStepV0 {
502                operation: "analyzeFlowGraph",
503                result_kind: "flowAnalysis",
504                reason: "ran the class-value flow graph inside the call-site context",
505            },
506            OneCfaCallSiteDerivationStepV0 {
507                operation: "projectExitNode",
508                result_kind: abstract_class_value_kind(exit_value),
509                reason: "projected the requested exit node as the call-site result",
510            },
511        ],
512    }
513}
514
515fn summarize_one_cfa_callees(
516    entries: &[OneCfaCallSiteFlowEntryV0],
517) -> Vec<OneCfaCalleeFlowSummaryV0> {
518    let mut by_callee = BTreeMap::<String, Vec<AbstractClassValueV0>>::new();
519    for entry in entries {
520        by_callee
521            .entry(entry.callee_key.clone())
522            .or_default()
523            .push(entry.exit_value.clone());
524    }
525
526    by_callee
527        .into_iter()
528        .map(|(callee_key, values)| {
529            let call_site_count = values.len();
530            let joined_exit_value = values
531                .into_iter()
532                .reduce(|left, right| join_abstract_class_values(&left, &right))
533                .unwrap_or_else(bottom_class_value);
534            OneCfaCalleeFlowSummaryV0 {
535                callee_key,
536                call_site_count,
537                joined_exit_value_kind: abstract_class_value_kind(&joined_exit_value),
538                joined_exit_value,
539            }
540        })
541        .collect()
542}
543
544fn summarize_k_limited_callees(
545    entries: &[KLimitedCallSiteFlowEntryV0],
546) -> Vec<OneCfaCalleeFlowSummaryV0> {
547    let mut by_callee = BTreeMap::<String, Vec<AbstractClassValueV0>>::new();
548    for entry in entries {
549        by_callee
550            .entry(entry.callee_key.clone())
551            .or_default()
552            .push(entry.exit_value.clone());
553    }
554
555    by_callee
556        .into_iter()
557        .map(|(callee_key, values)| {
558            let call_site_count = values.len();
559            let joined_exit_value = values
560                .into_iter()
561                .reduce(|left, right| join_abstract_class_values(&left, &right))
562                .unwrap_or_else(bottom_class_value);
563            OneCfaCalleeFlowSummaryV0 {
564                callee_key,
565                call_site_count,
566                joined_exit_value_kind: abstract_class_value_kind(&joined_exit_value),
567                joined_exit_value,
568            }
569        })
570        .collect()
571}
572
573fn join_predecessor_flow_values(
574    node: &ClassValueFlowNodeV0,
575    values: &BTreeMap<String, AbstractClassValueV0>,
576) -> AbstractClassValueV0 {
577    node.predecessors
578        .iter()
579        .map(|id| values.get(id).cloned().unwrap_or_else(top_class_value))
580        .reduce(|left, right| join_abstract_class_values(&left, &right))
581        .unwrap_or_else(bottom_class_value)
582}
583
584fn apply_flow_transfer(
585    incoming: &AbstractClassValueV0,
586    transfer: &ClassValueFlowTransferV0,
587) -> AbstractClassValueV0 {
588    match transfer {
589        ClassValueFlowTransferV0::AssignFacts(facts) => {
590            reduced_abstract_class_value_from_facts(facts)
591        }
592        ClassValueFlowTransferV0::RefineFacts(facts) => {
593            let refinement = reduced_abstract_class_value_from_facts(facts);
594            intersect_abstract_class_values(incoming, &refinement)
595        }
596        ClassValueFlowTransferV0::ConcatFacts(facts) => {
597            let right = reduced_abstract_class_value_from_facts(facts);
598            concatenate_abstract_class_values(incoming, &right)
599        }
600        ClassValueFlowTransferV0::Join => incoming.clone(),
601    }
602}
603
604fn flow_transfer_kind(transfer: &ClassValueFlowTransferV0) -> &'static str {
605    match transfer {
606        ClassValueFlowTransferV0::AssignFacts(_) => "assignFacts",
607        ClassValueFlowTransferV0::RefineFacts(_) => "refineFacts",
608        ClassValueFlowTransferV0::ConcatFacts(_) => "concatFacts",
609        ClassValueFlowTransferV0::Join => "join",
610    }
611}
612
613fn flow_node_incremental_digest(node: &ClassValueFlowNodeV0) -> String {
614    let mut parts = vec![
615        format!("id={}", node.id),
616        format!("deps={}", node.predecessors.join(",")),
617        format!("transfer={}", flow_transfer_kind(&node.transfer)),
618    ];
619
620    match &node.transfer {
621        ClassValueFlowTransferV0::AssignFacts(facts)
622        | ClassValueFlowTransferV0::RefineFacts(facts)
623        | ClassValueFlowTransferV0::ConcatFacts(facts) => {
624            push_external_facts_digest_parts(&mut parts, facts);
625        }
626        ClassValueFlowTransferV0::Join => {}
627    }
628
629    parts.join(";")
630}
631
632fn push_external_facts_digest_parts(parts: &mut Vec<String>, facts: &ExternalStringTypeFactsV0) {
633    parts.push(format!("kind={}", facts.kind));
634    parts.push(format!(
635        "constraint={}",
636        facts.constraint_kind.as_deref().unwrap_or("")
637    ));
638    parts.push(format!(
639        "values={}",
640        facts.values.as_ref().map_or_else(String::new, |values| {
641            let mut values = values.clone();
642            values.sort();
643            values.dedup();
644            values.join(",")
645        })
646    ));
647    parts.push(format!("prefix={}", facts.prefix.as_deref().unwrap_or("")));
648    parts.push(format!("suffix={}", facts.suffix.as_deref().unwrap_or("")));
649    parts.push(format!(
650        "minLen={}",
651        facts
652            .min_len
653            .map_or_else(String::new, |value| value.to_string())
654    ));
655    parts.push(format!(
656        "maxLen={}",
657        facts
658            .max_len
659            .map_or_else(String::new, |value| value.to_string())
660    ));
661    parts.push(format!(
662        "charMust={}",
663        facts.char_must.as_deref().unwrap_or("")
664    ));
665    parts.push(format!(
666        "charMay={}",
667        facts.char_may.as_deref().unwrap_or("")
668    ));
669    parts.push(format!(
670        "mayOther={}",
671        facts
672            .may_include_other_chars
673            .map_or_else(String::new, |value| value.to_string())
674    ));
675}