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}