Skip to main content

engine_input_producers/
expression_domain.rs

1use std::collections::BTreeMap;
2
3use crate::{
4    ConstraintDetailCounts, ConstraintDetailInput, EngineInputV2,
5    ExpressionDomainCallSiteFlowAnalysisV0, ExpressionDomainCandidateV0,
6    ExpressionDomainCandidatesV0, ExpressionDomainCanonicalCandidateBundleV0,
7    ExpressionDomainCanonicalProducerSignalV0, ExpressionDomainControlFlowAnalysisEntryV0,
8    ExpressionDomainControlFlowAnalysisV0, ExpressionDomainEvaluatorCandidatePayloadV0,
9    ExpressionDomainEvaluatorCandidateV0, ExpressionDomainEvaluatorCandidatesV0,
10    ExpressionDomainFlowAnalysisEntryV0, ExpressionDomainFlowAnalysisV0,
11    ExpressionDomainFlowGraphEntryV0, ExpressionDomainFragmentV0, ExpressionDomainFragmentsV0,
12    ExpressionDomainPlanSummaryV0, ExpressionDomainProvenanceExplanationV0,
13    ExpressionDomainProvenanceExplanationsV0, ExpressionDomainReducedProductIterationEntryV0,
14    ExpressionDomainReducedProductIterationV0, StringTypeFactsV2, TypeFactControlFlowBlockV2,
15    TypeFactControlFlowGraphV2, TypeFactEntryV2, abstract_value_facts,
16    collect_constraint_detail_counts, map_reduced_expression_value_domain_derivation,
17    map_reduced_expression_value_domain_kind, map_reduced_expression_value_domain_provenance_tree,
18};
19
20struct ExpressionDomainInputRows {
21    plan_summary: ExpressionDomainPlanSummaryV0,
22    fragments: Vec<ExpressionDomainFragmentV0>,
23    candidates: Vec<ExpressionDomainCandidateV0>,
24    evaluator_candidates: Vec<ExpressionDomainEvaluatorCandidateV0>,
25}
26
27fn collect_expression_domain_input_rows(input: &EngineInputV2) -> ExpressionDomainInputRows {
28    let mut planned_expression_ids = Vec::new();
29    let mut value_domain_kinds = BTreeMap::new();
30    let mut value_constraint_kinds = BTreeMap::new();
31    let mut constraint_detail_counts = ConstraintDetailCounts::default();
32    let mut finite_value_count = 0usize;
33    let mut fragments = Vec::new();
34    let mut candidates = Vec::new();
35    let mut evaluator_candidates = Vec::new();
36
37    for entry in &input.type_facts {
38        planned_expression_ids.push(entry.expression_id.clone());
39        *value_domain_kinds
40            .entry(entry.facts.kind.clone())
41            .or_insert(0) += 1;
42
43        if let Some(values) = &entry.facts.values {
44            finite_value_count += values.len();
45        }
46
47        if let Some(constraint_kind) = &entry.facts.constraint_kind {
48            *value_constraint_kinds
49                .entry(constraint_kind.clone())
50                .or_insert(0) += 1;
51        }
52
53        collect_constraint_detail_counts(
54            &mut constraint_detail_counts,
55            ConstraintDetailInput {
56                prefix: entry.facts.prefix.as_ref(),
57                suffix: entry.facts.suffix.as_ref(),
58                min_len: entry.facts.min_len,
59                max_len: entry.facts.max_len,
60                char_must: entry.facts.char_must.as_ref(),
61                char_may: entry.facts.char_may.as_ref(),
62                may_include_other_chars: entry.facts.may_include_other_chars,
63            },
64        );
65
66        let fragment = ExpressionDomainFragmentV0 {
67            expression_id: entry.expression_id.clone(),
68            file_path: entry.file_path.clone(),
69            value_domain_kind: entry.facts.kind.clone(),
70            value_constraint_kind: entry.facts.constraint_kind.clone(),
71            value_prefix: entry.facts.prefix.clone(),
72            value_suffix: entry.facts.suffix.clone(),
73            value_min_len: entry.facts.min_len,
74            value_max_len: entry.facts.max_len,
75            value_char_must: entry.facts.char_must.clone(),
76            value_char_may: entry.facts.char_may.clone(),
77            value_may_include_other_chars: entry.facts.may_include_other_chars,
78            finite_value_count: entry.facts.values.as_ref().map_or(0, Vec::len),
79        };
80        fragments.push(fragment.clone());
81        candidates.push(ExpressionDomainCandidateV0 {
82            expression_id: fragment.expression_id,
83            file_path: fragment.file_path,
84            value_domain_kind: fragment.value_domain_kind,
85            value_constraint_kind: fragment.value_constraint_kind,
86            value_prefix: fragment.value_prefix,
87            value_suffix: fragment.value_suffix,
88            value_min_len: fragment.value_min_len,
89            value_max_len: fragment.value_max_len,
90            value_char_must: fragment.value_char_must,
91            value_char_may: fragment.value_char_may,
92            value_may_include_other_chars: fragment.value_may_include_other_chars,
93            finite_value_count: fragment.finite_value_count,
94        });
95
96        evaluator_candidates.push(ExpressionDomainEvaluatorCandidateV0 {
97            kind: "expression-domain",
98            file_path: entry.file_path.clone(),
99            query_id: entry.expression_id.clone(),
100            payload: ExpressionDomainEvaluatorCandidatePayloadV0 {
101                expression_id: entry.expression_id.clone(),
102                value_domain_kind: map_reduced_expression_value_domain_kind(&entry.facts),
103                value_constraint_kind: entry.facts.constraint_kind.clone(),
104                value_prefix: entry.facts.prefix.clone(),
105                value_suffix: entry.facts.suffix.clone(),
106                value_min_len: entry.facts.min_len,
107                value_max_len: entry.facts.max_len,
108                value_char_must: entry.facts.char_must.clone(),
109                value_char_may: entry.facts.char_may.clone(),
110                value_may_include_other_chars: entry.facts.may_include_other_chars,
111                finite_value_count: entry.facts.values.as_ref().map_or(0, Vec::len),
112                value_domain_derivation: map_reduced_expression_value_domain_derivation(
113                    &entry.facts,
114                ),
115                value_domain_provenance_tree: map_reduced_expression_value_domain_provenance_tree(
116                    &entry.facts,
117                ),
118            },
119        });
120    }
121
122    fragments.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
123    candidates.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
124    evaluator_candidates.sort_by(|a, b| a.query_id.cmp(&b.query_id));
125
126    ExpressionDomainInputRows {
127        plan_summary: ExpressionDomainPlanSummaryV0 {
128            schema_version: "0",
129            input_version: input.version.clone(),
130            planned_expression_ids,
131            value_domain_kinds,
132            value_constraint_kinds,
133            constraint_detail_counts,
134            finite_value_count,
135        },
136        fragments,
137        candidates,
138        evaluator_candidates,
139    }
140}
141
142pub fn summarize_expression_domain_plan_input(
143    input: &EngineInputV2,
144) -> ExpressionDomainPlanSummaryV0 {
145    collect_expression_domain_input_rows(input).plan_summary
146}
147
148pub fn summarize_expression_domain_fragments_input(
149    input: &EngineInputV2,
150) -> ExpressionDomainFragmentsV0 {
151    let rows = collect_expression_domain_input_rows(input);
152
153    ExpressionDomainFragmentsV0 {
154        schema_version: "0",
155        input_version: input.version.clone(),
156        fragments: rows.fragments,
157    }
158}
159
160pub fn summarize_expression_domain_candidates_input(
161    input: &EngineInputV2,
162) -> ExpressionDomainCandidatesV0 {
163    let rows = collect_expression_domain_input_rows(input);
164
165    ExpressionDomainCandidatesV0 {
166        schema_version: "0",
167        input_version: input.version.clone(),
168        candidates: rows.candidates,
169    }
170}
171
172pub fn summarize_expression_domain_canonical_candidate_bundle_input(
173    input: &EngineInputV2,
174) -> ExpressionDomainCanonicalCandidateBundleV0 {
175    let rows = collect_expression_domain_input_rows(input);
176
177    ExpressionDomainCanonicalCandidateBundleV0 {
178        schema_version: "0",
179        input_version: input.version.clone(),
180        plan_summary: rows.plan_summary,
181        fragments: rows.fragments,
182        candidates: rows.candidates,
183    }
184}
185
186pub fn summarize_expression_domain_evaluator_candidates_input(
187    input: &EngineInputV2,
188) -> ExpressionDomainEvaluatorCandidatesV0 {
189    let rows = collect_expression_domain_input_rows(input);
190
191    ExpressionDomainEvaluatorCandidatesV0 {
192        schema_version: "0",
193        input_version: input.version.clone(),
194        results: rows.evaluator_candidates,
195    }
196}
197
198pub fn summarize_expression_domain_canonical_producer_signal_input(
199    input: &EngineInputV2,
200) -> ExpressionDomainCanonicalProducerSignalV0 {
201    let rows = collect_expression_domain_input_rows(input);
202    let input_version = input.version.clone();
203
204    ExpressionDomainCanonicalProducerSignalV0 {
205        schema_version: "0",
206        input_version: input_version.clone(),
207        canonical_bundle: ExpressionDomainCanonicalCandidateBundleV0 {
208            schema_version: "0",
209            input_version: input_version.clone(),
210            plan_summary: rows.plan_summary,
211            fragments: rows.fragments,
212            candidates: rows.candidates,
213        },
214        evaluator_candidates: ExpressionDomainEvaluatorCandidatesV0 {
215            schema_version: "0",
216            input_version,
217            results: rows.evaluator_candidates,
218        },
219    }
220}
221
222pub fn summarize_expression_domain_provenance_explanations_input(
223    input: &EngineInputV2,
224) -> ExpressionDomainProvenanceExplanationsV0 {
225    let explanations = input
226        .type_facts
227        .iter()
228        .map(|entry| {
229            let derivation = map_reduced_expression_value_domain_derivation(&entry.facts);
230            let provenance_tree = map_reduced_expression_value_domain_provenance_tree(&entry.facts);
231
232            ExpressionDomainProvenanceExplanationV0 {
233                expression_id: entry.expression_id.clone(),
234                file_path: entry.file_path.clone(),
235                input_fact_kind: derivation.input_fact_kind.clone(),
236                input_constraint_kind: derivation.input_constraint_kind.clone(),
237                reduced_kind: derivation.reduced_kind,
238                derivation,
239                provenance_tree,
240            }
241        })
242        .collect::<Vec<_>>();
243
244    ExpressionDomainProvenanceExplanationsV0 {
245        schema_version: "0",
246        product: "engine-input-producers.expression-domain-provenance-explanations",
247        input_version: input.version.clone(),
248        explanation_count: explanations.len(),
249        explanations,
250    }
251}
252
253pub fn summarize_expression_domain_flow_analysis_input(
254    input: &EngineInputV2,
255) -> ExpressionDomainFlowAnalysisV0 {
256    let analyses = collect_expression_domain_flow_graphs(input)
257        .into_iter()
258        .map(|entry| ExpressionDomainFlowAnalysisEntryV0 {
259            graph_id: entry.graph_id,
260            file_path: entry.file_path,
261            analysis: omena_abstract_value::analyze_class_value_flow(&entry.graph),
262        })
263        .collect();
264
265    ExpressionDomainFlowAnalysisV0 {
266        schema_version: "0",
267        product: "engine-input-producers.expression-domain-flow-analysis",
268        input_version: input.version.clone(),
269        analyses,
270    }
271}
272
273pub fn summarize_expression_domain_control_flow_analysis_input(
274    input: &EngineInputV2,
275) -> ExpressionDomainControlFlowAnalysisV0 {
276    let analyses = collect_expression_domain_control_flow_graphs(input)
277        .into_iter()
278        .map(|entry| ExpressionDomainControlFlowAnalysisEntryV0 {
279            graph_id: entry.graph_id,
280            file_path: entry.file_path,
281            analysis: omena_abstract_value::analyze_class_value_control_flow_graph(&entry.graph),
282        })
283        .collect();
284
285    ExpressionDomainControlFlowAnalysisV0 {
286        schema_version: "0",
287        product: "engine-input-producers.expression-domain-control-flow-analysis",
288        input_version: input.version.clone(),
289        analyses,
290    }
291}
292
293pub fn summarize_expression_domain_call_site_flow_analysis_input(
294    input: &EngineInputV2,
295) -> ExpressionDomainCallSiteFlowAnalysisV0 {
296    let call_site_inputs = collect_expression_domain_call_site_flow_inputs(input);
297
298    ExpressionDomainCallSiteFlowAnalysisV0 {
299        schema_version: "0",
300        product: "engine-input-producers.expression-domain-call-site-flow-analysis",
301        input_version: input.version.clone(),
302        zero_cfa: omena_abstract_value::analyze_k_limited_call_site_flows(&call_site_inputs, 0),
303        one_cfa: omena_abstract_value::analyze_k_limited_call_site_flows(&call_site_inputs, 1),
304    }
305}
306
307pub fn summarize_expression_domain_reduced_product_iteration_input(
308    input: &EngineInputV2,
309) -> ExpressionDomainReducedProductIterationV0 {
310    let iterations = input
311        .type_facts
312        .iter()
313        .filter_map(|entry| {
314            let axis_constraints = reduced_product_axis_constraints_from_facts(&entry.facts);
315            (!axis_constraints.is_empty()).then(|| {
316                let iteration =
317                    omena_abstract_value::iterate_reduced_class_value_product_constraints(
318                        &axis_constraints,
319                    );
320                ExpressionDomainReducedProductIterationEntryV0 {
321                    expression_id: entry.expression_id.clone(),
322                    file_path: entry.file_path.clone(),
323                    input_value_kind: map_reduced_expression_value_domain_kind(&entry.facts),
324                    axis_constraint_count: axis_constraints.len(),
325                    iteration,
326                }
327            })
328        })
329        .collect::<Vec<_>>();
330
331    ExpressionDomainReducedProductIterationV0 {
332        schema_version: "0",
333        product: "engine-input-producers.expression-domain-reduced-product-iteration",
334        input_version: input.version.clone(),
335        iteration_count: iterations.len(),
336        iterations,
337    }
338}
339
340pub fn collect_expression_domain_flow_graphs(
341    input: &EngineInputV2,
342) -> Vec<ExpressionDomainFlowGraphEntryV0> {
343    input
344        .type_facts
345        .iter()
346        .map(|entry| {
347            let graph_id = format!(
348                "{}:{}:expression-domain-flow",
349                entry.file_path, entry.expression_id
350            );
351            let graph = omena_abstract_value::ClassValueFlowGraphV0 {
352                context_key: Some(graph_id.clone()),
353                nodes: vec![omena_abstract_value::ClassValueFlowNodeV0 {
354                    id: entry.expression_id.clone(),
355                    predecessors: Vec::new(),
356                    transfer: omena_abstract_value::ClassValueFlowTransferV0::AssignFacts(
357                        abstract_value_facts(&entry.facts),
358                    ),
359                }],
360            };
361
362            ExpressionDomainFlowGraphEntryV0 {
363                graph_id,
364                file_path: entry.file_path.clone(),
365                graph,
366            }
367        })
368        .collect()
369}
370
371#[derive(Debug, Clone, PartialEq, Eq)]
372struct ExpressionDomainControlFlowGraphEntryV0 {
373    graph_id: String,
374    file_path: String,
375    graph: omena_abstract_value::ClassValueControlFlowGraphV0,
376}
377
378fn collect_expression_domain_control_flow_graphs(
379    input: &EngineInputV2,
380) -> Vec<ExpressionDomainControlFlowGraphEntryV0> {
381    input
382        .type_facts
383        .iter()
384        .filter_map(|entry| {
385            entry.control_flow_graph.as_ref().map(|graph| {
386                let graph_id = format!(
387                    "{}:{}:expression-domain-control-flow",
388                    entry.file_path, entry.expression_id
389                );
390                ExpressionDomainControlFlowGraphEntryV0 {
391                    graph_id: graph_id.clone(),
392                    file_path: entry.file_path.clone(),
393                    graph: expression_domain_control_flow_graph_from_type_fact_graph(
394                        &graph_id, entry, graph,
395                    ),
396                }
397            })
398        })
399        .collect()
400}
401
402fn collect_expression_domain_call_site_flow_inputs(
403    input: &EngineInputV2,
404) -> Vec<omena_abstract_value::KLimitedCallSiteFlowInputV0> {
405    collect_expression_domain_flow_graphs(input)
406        .into_iter()
407        .map(|entry| {
408            let exit_node_id = expression_domain_flow_exit_node_id(&entry.graph);
409            omena_abstract_value::KLimitedCallSiteFlowInputV0 {
410                callee_key: "expression-domain-class-value".to_string(),
411                call_site_stack: vec![entry.file_path, entry.graph_id],
412                graph: entry.graph,
413                exit_node_id,
414            }
415        })
416        .collect()
417}
418
419fn reduced_product_axis_constraints_from_facts(
420    facts: &StringTypeFactsV2,
421) -> Vec<omena_abstract_value::AbstractClassValueV0> {
422    let mut constraints = Vec::new();
423
424    if let Some(prefix) = &facts.prefix {
425        constraints.push(omena_abstract_value::prefix_class_value(
426            prefix.clone(),
427            None,
428        ));
429    }
430
431    if let Some(suffix) = &facts.suffix {
432        constraints.push(omena_abstract_value::suffix_class_value(
433            suffix.clone(),
434            None,
435        ));
436    }
437
438    if facts.char_must.is_some()
439        || facts.char_may.is_some()
440        || facts.may_include_other_chars.is_some()
441    {
442        constraints.push(omena_abstract_value::char_inclusion_class_value(
443            facts.char_must.clone().unwrap_or_default(),
444            facts.char_may.clone().unwrap_or_default(),
445            None,
446            facts.may_include_other_chars.unwrap_or(false),
447        ));
448    }
449
450    constraints
451}
452
453fn expression_domain_flow_exit_node_id(
454    graph: &omena_abstract_value::ClassValueFlowGraphV0,
455) -> String {
456    graph
457        .nodes
458        .first()
459        .map(|node| node.id.clone())
460        .unwrap_or_else(|| "exit".to_string())
461}
462
463fn expression_domain_control_flow_graph_from_type_fact_graph(
464    graph_id: &str,
465    entry: &TypeFactEntryV2,
466    graph: &TypeFactControlFlowGraphV2,
467) -> omena_abstract_value::ClassValueControlFlowGraphV0 {
468    let predecessor_block_ids = control_flow_predecessor_block_ids(&graph.blocks);
469    let block_node_ids = graph
470        .blocks
471        .iter()
472        .map(|block| {
473            (
474                block.id.clone(),
475                type_fact_control_flow_node_id(entry, block),
476            )
477        })
478        .collect::<BTreeMap<_, _>>();
479    let blocks = graph
480        .blocks
481        .iter()
482        .map(|block| {
483            let node_id = block_node_ids
484                .get(&block.id)
485                .cloned()
486                .unwrap_or_else(|| type_fact_control_flow_node_id(entry, block));
487            let predecessors = predecessor_block_ids
488                .get(&block.id)
489                .into_iter()
490                .flat_map(|ids| ids.iter())
491                .filter_map(|id| block_node_ids.get(id).cloned())
492                .collect::<Vec<_>>();
493            omena_abstract_value::ClassValueControlFlowBlockV0 {
494                id: block.id.clone(),
495                nodes: vec![omena_abstract_value::ClassValueFlowNodeV0 {
496                    id: node_id,
497                    predecessors,
498                    transfer: type_fact_control_flow_transfer(block, &entry.facts),
499                }],
500                successor_block_ids: block.successor_block_ids.clone(),
501            }
502        })
503        .collect();
504
505    omena_abstract_value::ClassValueControlFlowGraphV0 {
506        context_key: Some(graph_id.to_string()),
507        entry_block_id: graph.entry_block_id.clone(),
508        blocks,
509    }
510}
511
512fn control_flow_predecessor_block_ids(
513    blocks: &[TypeFactControlFlowBlockV2],
514) -> BTreeMap<String, Vec<String>> {
515    let mut predecessors = BTreeMap::<String, Vec<String>>::new();
516    for block in blocks {
517        for successor in &block.successor_block_ids {
518            predecessors
519                .entry(successor.clone())
520                .or_default()
521                .push(block.id.clone());
522        }
523    }
524    predecessors
525}
526
527fn type_fact_control_flow_node_id(
528    entry: &TypeFactEntryV2,
529    block: &TypeFactControlFlowBlockV2,
530) -> String {
531    format!("{}:{}", entry.expression_id, block.id)
532}
533
534fn type_fact_control_flow_transfer(
535    block: &TypeFactControlFlowBlockV2,
536    facts: &StringTypeFactsV2,
537) -> omena_abstract_value::ClassValueFlowTransferV0 {
538    if let Some(block_facts) = &block.facts
539        && matches!(block.transfer_kind.as_str(), "assignFacts" | "concatFacts")
540    {
541        return omena_abstract_value::ClassValueFlowTransferV0::AssignFacts(abstract_value_facts(
542            block_facts,
543        ));
544    }
545
546    match block.transfer_kind.as_str() {
547        "assignFacts" => {
548            omena_abstract_value::ClassValueFlowTransferV0::AssignFacts(abstract_value_facts(facts))
549        }
550        "concatFacts" => {
551            omena_abstract_value::ClassValueFlowTransferV0::ConcatFacts(abstract_value_facts(facts))
552        }
553        _ => omena_abstract_value::ClassValueFlowTransferV0::Join,
554    }
555}
556
557#[cfg(test)]
558mod tests {
559    use super::{
560        collect_expression_domain_flow_graphs,
561        summarize_expression_domain_call_site_flow_analysis_input,
562        summarize_expression_domain_candidates_input,
563        summarize_expression_domain_canonical_candidate_bundle_input,
564        summarize_expression_domain_canonical_producer_signal_input,
565        summarize_expression_domain_control_flow_analysis_input,
566        summarize_expression_domain_evaluator_candidates_input,
567        summarize_expression_domain_flow_analysis_input,
568        summarize_expression_domain_fragments_input, summarize_expression_domain_plan_input,
569        summarize_expression_domain_provenance_explanations_input,
570        summarize_expression_domain_reduced_product_iteration_input,
571    };
572    use crate::{
573        StringTypeFactsV2, TypeFactControlFlowBlockV2, TypeFactControlFlowGraphV2, TypeFactEntryV2,
574        test_support::sample_input,
575    };
576    use omena_abstract_value::AbstractClassValueV0;
577
578    #[test]
579    fn summarizes_expression_domain_counts() {
580        let summary = summarize_expression_domain_plan_input(&sample_input());
581
582        assert_eq!(
583            summary.planned_expression_ids,
584            vec!["expr-1".to_string(), "expr-2".to_string()]
585        );
586        assert_eq!(summary.value_domain_kinds.get("constrained"), Some(&1));
587        assert_eq!(summary.value_domain_kinds.get("finiteSet"), Some(&1));
588        assert_eq!(summary.value_constraint_kinds.get("prefixSuffix"), Some(&1));
589        assert_eq!(summary.constraint_detail_counts.prefix_count, 1);
590        assert_eq!(summary.constraint_detail_counts.suffix_count, 1);
591        assert_eq!(summary.constraint_detail_counts.min_len_count, 1);
592        assert_eq!(summary.finite_value_count, 2);
593    }
594
595    #[test]
596    fn summarizes_expression_domain_fragments() {
597        let summary = summarize_expression_domain_fragments_input(&sample_input());
598
599        assert_eq!(summary.fragments.len(), 2);
600        let first = &summary.fragments[0];
601        assert_eq!(first.expression_id, "expr-1");
602        assert_eq!(first.file_path, "/tmp/App.tsx");
603        assert_eq!(first.value_domain_kind, "constrained");
604        assert_eq!(first.value_constraint_kind.as_deref(), Some("prefixSuffix"));
605        assert_eq!(first.value_prefix.as_deref(), Some("btn-"));
606        assert_eq!(first.value_suffix.as_deref(), Some("-active"));
607        assert_eq!(first.value_min_len, Some(10));
608        assert_eq!(first.finite_value_count, 0);
609
610        let second = &summary.fragments[1];
611        assert_eq!(second.expression_id, "expr-2");
612        assert_eq!(second.value_domain_kind, "finiteSet");
613        assert_eq!(second.finite_value_count, 2);
614    }
615
616    #[test]
617    fn summarizes_expression_domain_candidates() {
618        let summary = summarize_expression_domain_candidates_input(&sample_input());
619
620        assert_eq!(summary.candidates.len(), 2);
621        assert_eq!(summary.candidates[0].expression_id, "expr-1");
622        assert_eq!(summary.candidates[0].value_domain_kind, "constrained");
623        assert_eq!(
624            summary.candidates[0].value_constraint_kind.as_deref(),
625            Some("prefixSuffix")
626        );
627        assert_eq!(summary.candidates[1].expression_id, "expr-2");
628        assert_eq!(summary.candidates[1].finite_value_count, 2);
629    }
630
631    #[test]
632    fn summarizes_expression_domain_canonical_candidate_bundle() {
633        let summary = summarize_expression_domain_canonical_candidate_bundle_input(&sample_input());
634
635        assert_eq!(summary.plan_summary.planned_expression_ids.len(), 2);
636        assert_eq!(summary.fragments.len(), 2);
637        assert_eq!(summary.candidates.len(), 2);
638    }
639
640    #[test]
641    fn summarizes_expression_domain_evaluator_candidates() {
642        let summary = summarize_expression_domain_evaluator_candidates_input(&sample_input());
643
644        assert_eq!(summary.schema_version, "0");
645        assert_eq!(summary.input_version, "2");
646        assert_eq!(summary.results.len(), 2);
647        assert_eq!(summary.results[0].kind, "expression-domain");
648        assert_eq!(summary.results[0].query_id, "expr-1");
649        assert_eq!(summary.results[0].payload.value_domain_kind, "prefixSuffix");
650        assert_eq!(
651            summary.results[0].payload.value_constraint_kind.as_deref(),
652            Some("prefixSuffix")
653        );
654        assert_eq!(summary.results[1].payload.finite_value_count, 2);
655    }
656
657    #[test]
658    fn summarizes_expression_domain_provenance_explanations() {
659        let summary = summarize_expression_domain_provenance_explanations_input(&sample_input());
660
661        assert_eq!(summary.schema_version, "0");
662        assert_eq!(
663            summary.product,
664            "engine-input-producers.expression-domain-provenance-explanations"
665        );
666        assert_eq!(summary.input_version, "2");
667        assert_eq!(summary.explanation_count, 2);
668        assert_eq!(summary.explanations[0].expression_id, "expr-1");
669        assert_eq!(summary.explanations[0].input_fact_kind, "constrained");
670        assert_eq!(
671            summary.explanations[0].input_constraint_kind.as_deref(),
672            Some("prefixSuffix")
673        );
674        assert_eq!(summary.explanations[0].reduced_kind, "prefixSuffix");
675        assert_eq!(
676            summary.explanations[0].derivation.product,
677            "omena-abstract-value.reduced-class-value-derivation"
678        );
679        assert_eq!(
680            summary.explanations[0].provenance_tree.product,
681            "omena-abstract-value.provenance-tree"
682        );
683        assert_eq!(
684            summary.explanations[0].provenance_tree.root.operation,
685            "constraintDomain"
686        );
687    }
688
689    #[test]
690    fn expression_domain_evaluator_reports_reduced_value_domain_kind() {
691        let mut input = sample_input();
692        input.type_facts.push(TypeFactEntryV2 {
693            file_path: "/tmp/App.tsx".to_string(),
694            expression_id: "expr-3".to_string(),
695            facts: StringTypeFactsV2 {
696                kind: "finiteSet".to_string(),
697                constraint_kind: Some("prefix".to_string()),
698                values: Some(vec!["btn-active".to_string(), "card".to_string()]),
699                prefix: Some("btn-".to_string()),
700                suffix: None,
701                min_len: None,
702                max_len: None,
703                char_must: None,
704                char_may: None,
705                may_include_other_chars: None,
706                provenance: None,
707            },
708            control_flow_graph: None,
709        });
710
711        let fragments = summarize_expression_domain_fragments_input(&input);
712        let candidates = summarize_expression_domain_candidates_input(&input);
713        let evaluator_candidates = summarize_expression_domain_evaluator_candidates_input(&input);
714
715        assert_eq!(fragments.fragments[2].expression_id, "expr-3");
716        assert_eq!(fragments.fragments[2].value_domain_kind, "finiteSet");
717        assert_eq!(candidates.candidates[2].expression_id, "expr-3");
718        assert_eq!(candidates.candidates[2].value_domain_kind, "finiteSet");
719        assert_eq!(evaluator_candidates.results[2].query_id, "expr-3");
720        assert_eq!(
721            evaluator_candidates.results[2].payload.value_domain_kind,
722            "exact"
723        );
724        assert_eq!(
725            evaluator_candidates.results[2]
726                .payload
727                .value_domain_derivation
728                .reduced_kind,
729            "exact"
730        );
731        assert_eq!(
732            evaluator_candidates.results[2]
733                .payload
734                .value_domain_derivation
735                .steps[1]
736                .operation,
737            "intersectConstraint"
738        );
739        assert_eq!(
740            evaluator_candidates.results[2]
741                .payload
742                .value_domain_provenance_tree
743                .product,
744            "omena-abstract-value.provenance-tree"
745        );
746        assert_eq!(
747            evaluator_candidates.results[2]
748                .payload
749                .value_domain_provenance_tree
750                .root
751                .operation,
752            "exactLiteral"
753        );
754    }
755
756    #[test]
757    fn summarizes_expression_domain_flow_analysis() {
758        let mut input = sample_input();
759        input.type_facts = vec![
760            exact_type_fact("expr-branch-a", "btn-primary"),
761            exact_type_fact("expr-branch-b", "btn-secondary"),
762            exact_type_fact("expr-branch-c", "card"),
763        ];
764
765        let summary = summarize_expression_domain_flow_analysis_input(&input);
766
767        assert_eq!(summary.schema_version, "0");
768        assert_eq!(
769            summary.product,
770            "engine-input-producers.expression-domain-flow-analysis"
771        );
772        assert_eq!(summary.analyses.len(), 3);
773        assert_eq!(summary.analyses[0].file_path, "/tmp/App.tsx");
774        assert_eq!(summary.analyses[0].analysis.context_sensitivity, "1-cfa");
775        assert!(
776            summary
777                .analyses
778                .iter()
779                .all(|entry| entry.analysis.converged)
780        );
781        assert_eq!(
782            summary.analyses[0]
783                .analysis
784                .nodes
785                .iter()
786                .find(|node| node.id == "expr-branch-a")
787                .map(|node| (node.value_kind, &node.value)),
788            Some((
789                "exact",
790                &AbstractClassValueV0::Exact {
791                    value: "btn-primary".to_string()
792                }
793            ))
794        );
795        assert!(summary.analyses.iter().all(|entry| {
796            entry
797                .analysis
798                .nodes
799                .iter()
800                .all(|node| node.id != "file-merge")
801        }));
802    }
803
804    #[test]
805    fn exposes_expression_domain_flow_graphs_for_query_runtime_reuse() {
806        let mut input = sample_input();
807        input.type_facts = vec![
808            exact_type_fact("expr-branch-a", "btn-primary"),
809            exact_type_fact("expr-branch-b", "btn-secondary"),
810        ];
811
812        let graphs = collect_expression_domain_flow_graphs(&input);
813
814        assert_eq!(graphs.len(), 2);
815        assert_eq!(
816            graphs
817                .iter()
818                .map(|entry| entry.graph_id.as_str())
819                .collect::<Vec<_>>(),
820            vec![
821                "/tmp/App.tsx:expr-branch-a:expression-domain-flow",
822                "/tmp/App.tsx:expr-branch-b:expression-domain-flow"
823            ]
824        );
825        assert!(graphs.iter().all(|entry| {
826            entry.graph.context_key.as_deref() == Some(entry.graph_id.as_str())
827                && entry.graph.nodes.iter().all(|node| node.id != "file-merge")
828        }));
829    }
830
831    #[test]
832    fn does_not_synthesize_control_flow_analysis_without_source_cfg() {
833        let mut input = sample_input();
834        input.type_facts = vec![
835            exact_type_fact("expr-branch-a", "btn-primary"),
836            exact_type_fact("expr-branch-b", "btn-secondary"),
837        ];
838
839        let summary = summarize_expression_domain_control_flow_analysis_input(&input);
840
841        assert_eq!(
842            summary.product,
843            "engine-input-producers.expression-domain-control-flow-analysis"
844        );
845        assert!(summary.analyses.is_empty());
846    }
847
848    #[test]
849    fn consumes_type_fact_control_flow_graph_for_branchy_flow() {
850        let mut input = sample_input();
851        input.type_facts = vec![TypeFactEntryV2 {
852            file_path: "/tmp/App.tsx".to_string(),
853            expression_id: "expr-branchy".to_string(),
854            facts: StringTypeFactsV2 {
855                kind: "exact".to_string(),
856                constraint_kind: None,
857                values: Some(vec!["btn-primary".to_string()]),
858                prefix: None,
859                suffix: None,
860                min_len: None,
861                max_len: None,
862                char_must: None,
863                char_may: None,
864                may_include_other_chars: None,
865                provenance: None,
866            },
867            control_flow_graph: Some(branchy_type_fact_control_flow_graph()),
868        }];
869
870        let summary = summarize_expression_domain_control_flow_analysis_input(&input);
871
872        assert_eq!(summary.analyses.len(), 1);
873        assert_eq!(
874            summary.analyses[0].graph_id,
875            "/tmp/App.tsx:expr-branchy:expression-domain-control-flow"
876        );
877        let analysis = &summary.analyses[0].analysis;
878        assert_eq!(analysis.block_count, 6);
879        assert_eq!(analysis.edge_count, 6);
880        assert_eq!(analysis.branch_block_ids, vec!["branch:0".to_string()]);
881        assert_eq!(analysis.join_block_ids, vec!["join:0".to_string()]);
882        assert!(
883            analysis
884                .blocks
885                .iter()
886                .all(|block| block.block_id != "file-merge")
887        );
888        assert!(
889            analysis
890                .blocks
891                .iter()
892                .find(|block| block.block_id == "branch:0")
893                .is_some_and(|block| block.successor_block_ids.len() > 1)
894        );
895        assert!(analysis
896            .flow_analysis
897            .nodes
898            .iter()
899            .any(|node| node.id == "expr-branchy:then:0"
900                && node.transfer_kind == "concatFacts"));
901    }
902
903    #[test]
904    fn control_flow_blocks_prefer_source_frontend_facts_when_present() {
905        let mut graph = branchy_type_fact_control_flow_graph();
906        if let Some(block) = graph.blocks.iter_mut().find(|block| block.id == "then:0") {
907            block.facts = Some(exact_type_fact("expr-block", "btn-secondary").facts);
908        }
909        let mut input = sample_input();
910        input.type_facts = vec![TypeFactEntryV2 {
911            file_path: "/tmp/App.tsx".to_string(),
912            expression_id: "expr-branchy".to_string(),
913            facts: StringTypeFactsV2 {
914                kind: "unknown".to_string(),
915                constraint_kind: None,
916                values: None,
917                prefix: None,
918                suffix: None,
919                min_len: None,
920                max_len: None,
921                char_must: None,
922                char_may: None,
923                may_include_other_chars: None,
924                provenance: None,
925            },
926            control_flow_graph: Some(graph),
927        }];
928
929        let summary = summarize_expression_domain_control_flow_analysis_input(&input);
930        let matching_nodes = summary.analyses[0]
931            .analysis
932            .flow_analysis
933            .nodes
934            .iter()
935            .filter(|node| node.id == "expr-branchy:then:0")
936            .collect::<Vec<_>>();
937        assert_eq!(matching_nodes.len(), 1);
938        let node = matching_nodes[0];
939
940        assert_eq!(node.transfer_kind, "assignFacts");
941        assert_eq!(
942            node.value,
943            AbstractClassValueV0::Exact {
944                value: "btn-secondary".to_string()
945            }
946        );
947    }
948
949    #[test]
950    fn summarizes_expression_domain_call_site_flow_analysis_for_zero_and_one_cfa() {
951        let mut input = sample_input();
952        input.type_facts = vec![
953            exact_type_fact_in_file("/tmp/App.tsx", "expr-primary", "btn-primary"),
954            exact_type_fact_in_file("/tmp/Card.tsx", "expr-secondary", "btn-secondary"),
955        ];
956
957        let summary = summarize_expression_domain_call_site_flow_analysis_input(&input);
958
959        assert_eq!(summary.schema_version, "0");
960        assert_eq!(
961            summary.product,
962            "engine-input-producers.expression-domain-call-site-flow-analysis"
963        );
964        assert_eq!(summary.zero_cfa.context_sensitivity, "0-cfa");
965        assert_eq!(summary.one_cfa.context_sensitivity, "1-cfa");
966        assert_eq!(summary.zero_cfa.call_site_count, 2);
967        assert_eq!(summary.one_cfa.call_site_count, 2);
968        assert_eq!(
969            summary.zero_cfa.entries[0].context_key,
970            "expression-domain-class-value@<root>"
971        );
972        assert_eq!(
973            summary.zero_cfa.entries[1].context_key,
974            "expression-domain-class-value@<root>"
975        );
976        assert_ne!(
977            summary.one_cfa.entries[0].context_key,
978            summary.one_cfa.entries[1].context_key
979        );
980        assert_eq!(
981            summary.zero_cfa.entries[0].exit_value,
982            AbstractClassValueV0::FiniteSet {
983                values: vec!["btn-primary".to_string(), "btn-secondary".to_string()]
984            }
985        );
986        assert_eq!(
987            summary.zero_cfa.entries[1].exit_value,
988            summary.zero_cfa.entries[0].exit_value
989        );
990        assert_eq!(
991            summary.one_cfa.entries[0].exit_value,
992            AbstractClassValueV0::Exact {
993                value: "btn-primary".to_string()
994            }
995        );
996        assert_eq!(
997            summary.one_cfa.entries[1].exit_value,
998            AbstractClassValueV0::Exact {
999                value: "btn-secondary".to_string()
1000            }
1001        );
1002    }
1003
1004    #[test]
1005    fn summarizes_expression_domain_reduced_product_iteration() {
1006        let mut input = sample_input();
1007        input.type_facts = vec![TypeFactEntryV2 {
1008            file_path: "/tmp/App.tsx".to_string(),
1009            expression_id: "expr-reduced".to_string(),
1010            facts: StringTypeFactsV2 {
1011                kind: "constrained".to_string(),
1012                constraint_kind: Some("composite".to_string()),
1013                values: None,
1014                prefix: Some("btn-".to_string()),
1015                suffix: Some("-active".to_string()),
1016                min_len: None,
1017                max_len: None,
1018                char_must: Some("a".to_string()),
1019                char_may: Some("-abceintv".to_string()),
1020                may_include_other_chars: Some(false),
1021                provenance: None,
1022            },
1023            control_flow_graph: None,
1024        }];
1025
1026        let summary = summarize_expression_domain_reduced_product_iteration_input(&input);
1027
1028        assert_eq!(summary.schema_version, "0");
1029        assert_eq!(
1030            summary.product,
1031            "engine-input-producers.expression-domain-reduced-product-iteration"
1032        );
1033        assert_eq!(summary.input_version, "2");
1034        assert_eq!(summary.iteration_count, 1);
1035        assert_eq!(summary.iterations[0].expression_id, "expr-reduced");
1036        assert_eq!(summary.iterations[0].axis_constraint_count, 3);
1037        assert_eq!(summary.iterations[0].input_value_kind, "composite");
1038        assert_eq!(summary.iterations[0].iteration.input_count, 3);
1039        assert_eq!(summary.iterations[0].iteration.applied_constraint_count, 3);
1040        assert!(summary.iterations[0].iteration.converged);
1041        assert!(summary.iterations[0].iteration.monotone_witness_valid);
1042        assert_eq!(summary.iterations[0].iteration.result_kind, "composite");
1043    }
1044
1045    #[test]
1046    fn summarizes_expression_domain_canonical_producer_signal() {
1047        let summary = summarize_expression_domain_canonical_producer_signal_input(&sample_input());
1048
1049        assert_eq!(summary.schema_version, "0");
1050        assert_eq!(summary.input_version, "2");
1051        assert_eq!(
1052            summary
1053                .canonical_bundle
1054                .plan_summary
1055                .planned_expression_ids
1056                .len(),
1057            2
1058        );
1059        assert_eq!(summary.canonical_bundle.fragments.len(), 2);
1060        assert_eq!(summary.canonical_bundle.candidates.len(), 2);
1061        assert_eq!(summary.evaluator_candidates.results.len(), 2);
1062    }
1063
1064    fn exact_type_fact(expression_id: &str, value: &str) -> TypeFactEntryV2 {
1065        exact_type_fact_in_file("/tmp/App.tsx", expression_id, value)
1066    }
1067
1068    fn exact_type_fact_in_file(
1069        file_path: &str,
1070        expression_id: &str,
1071        value: &str,
1072    ) -> TypeFactEntryV2 {
1073        TypeFactEntryV2 {
1074            file_path: file_path.to_string(),
1075            expression_id: expression_id.to_string(),
1076            facts: StringTypeFactsV2 {
1077                kind: "exact".to_string(),
1078                constraint_kind: None,
1079                values: Some(vec![value.to_string()]),
1080                prefix: None,
1081                suffix: None,
1082                min_len: None,
1083                max_len: None,
1084                char_must: None,
1085                char_may: None,
1086                may_include_other_chars: None,
1087                provenance: None,
1088            },
1089            control_flow_graph: None,
1090        }
1091    }
1092
1093    fn branchy_type_fact_control_flow_graph() -> TypeFactControlFlowGraphV2 {
1094        TypeFactControlFlowGraphV2 {
1095            entry_block_id: "entry".to_string(),
1096            blocks: vec![
1097                type_fact_control_flow_block("entry", "entry", "entry", &["branch:0"]),
1098                type_fact_control_flow_block("branch:0", "branch", "branch", &["then:0", "else:0"]),
1099                type_fact_control_flow_block("then:0", "assignment", "concatFacts", &["join:0"]),
1100                type_fact_control_flow_block("else:0", "assignment", "assignFacts", &["join:0"]),
1101                type_fact_control_flow_block("join:0", "join", "join", &["exit"]),
1102                type_fact_control_flow_block("exit", "exit", "exit", &[]),
1103            ],
1104        }
1105    }
1106
1107    fn type_fact_control_flow_block(
1108        id: &str,
1109        kind: &str,
1110        transfer_kind: &str,
1111        successor_block_ids: &[&str],
1112    ) -> TypeFactControlFlowBlockV2 {
1113        TypeFactControlFlowBlockV2 {
1114            id: id.to_string(),
1115            kind: kind.to_string(),
1116            transfer_kind: transfer_kind.to_string(),
1117            successor_block_ids: successor_block_ids
1118                .iter()
1119                .map(|id| (*id).to_string())
1120                .collect(),
1121            symbol_ordinal: None,
1122            variable_name: None,
1123            expression_kind: None,
1124            facts: None,
1125        }
1126    }
1127}