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, TypeFactEntryV2,
15    abstract_value_facts, collect_constraint_detail_counts,
16    map_reduced_expression_value_domain_derivation, map_reduced_expression_value_domain_kind,
17    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_flow_graphs(input)
277        .into_iter()
278        .map(|entry| {
279            let cfg = expression_domain_control_flow_graph(&entry.graph);
280            ExpressionDomainControlFlowAnalysisEntryV0 {
281                graph_id: entry.graph_id,
282                file_path: entry.file_path,
283                analysis: omena_abstract_value::analyze_class_value_control_flow_graph(&cfg),
284            }
285        })
286        .collect();
287
288    ExpressionDomainControlFlowAnalysisV0 {
289        schema_version: "0",
290        product: "engine-input-producers.expression-domain-control-flow-analysis",
291        input_version: input.version.clone(),
292        analyses,
293    }
294}
295
296pub fn summarize_expression_domain_call_site_flow_analysis_input(
297    input: &EngineInputV2,
298) -> ExpressionDomainCallSiteFlowAnalysisV0 {
299    let call_site_inputs = collect_expression_domain_call_site_flow_inputs(input);
300
301    ExpressionDomainCallSiteFlowAnalysisV0 {
302        schema_version: "0",
303        product: "engine-input-producers.expression-domain-call-site-flow-analysis",
304        input_version: input.version.clone(),
305        zero_cfa: omena_abstract_value::analyze_k_limited_call_site_flows(&call_site_inputs, 0),
306        one_cfa: omena_abstract_value::analyze_k_limited_call_site_flows(&call_site_inputs, 1),
307    }
308}
309
310pub fn summarize_expression_domain_reduced_product_iteration_input(
311    input: &EngineInputV2,
312) -> ExpressionDomainReducedProductIterationV0 {
313    let iterations = input
314        .type_facts
315        .iter()
316        .filter_map(|entry| {
317            let axis_constraints = reduced_product_axis_constraints_from_facts(&entry.facts);
318            (!axis_constraints.is_empty()).then(|| {
319                let iteration =
320                    omena_abstract_value::iterate_reduced_class_value_product_constraints(
321                        &axis_constraints,
322                    );
323                ExpressionDomainReducedProductIterationEntryV0 {
324                    expression_id: entry.expression_id.clone(),
325                    file_path: entry.file_path.clone(),
326                    input_value_kind: map_reduced_expression_value_domain_kind(&entry.facts),
327                    axis_constraint_count: axis_constraints.len(),
328                    iteration,
329                }
330            })
331        })
332        .collect::<Vec<_>>();
333
334    ExpressionDomainReducedProductIterationV0 {
335        schema_version: "0",
336        product: "engine-input-producers.expression-domain-reduced-product-iteration",
337        input_version: input.version.clone(),
338        iteration_count: iterations.len(),
339        iterations,
340    }
341}
342
343pub fn collect_expression_domain_flow_graphs(
344    input: &EngineInputV2,
345) -> Vec<ExpressionDomainFlowGraphEntryV0> {
346    let mut by_file = BTreeMap::<String, Vec<&TypeFactEntryV2>>::new();
347    for entry in &input.type_facts {
348        by_file
349            .entry(entry.file_path.clone())
350            .or_default()
351            .push(entry);
352    }
353
354    by_file
355        .into_iter()
356        .map(|(file_path, mut entries)| {
357            entries.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
358            let graph_id = format!("{file_path}:expression-domain-flow");
359            let mut nodes = entries
360                .iter()
361                .map(|entry| omena_abstract_value::ClassValueFlowNodeV0 {
362                    id: entry.expression_id.clone(),
363                    predecessors: Vec::new(),
364                    transfer: omena_abstract_value::ClassValueFlowTransferV0::AssignFacts(
365                        abstract_value_facts(&entry.facts),
366                    ),
367                })
368                .collect::<Vec<_>>();
369
370            if entries.len() > 1 {
371                nodes.push(omena_abstract_value::ClassValueFlowNodeV0 {
372                    id: "file-merge".to_string(),
373                    predecessors: entries
374                        .iter()
375                        .map(|entry| entry.expression_id.clone())
376                        .collect(),
377                    transfer: omena_abstract_value::ClassValueFlowTransferV0::Join,
378                });
379            }
380
381            let graph = omena_abstract_value::ClassValueFlowGraphV0 {
382                context_key: Some(graph_id.clone()),
383                nodes,
384            };
385
386            ExpressionDomainFlowGraphEntryV0 {
387                graph_id,
388                file_path,
389                graph,
390            }
391        })
392        .collect()
393}
394
395fn collect_expression_domain_call_site_flow_inputs(
396    input: &EngineInputV2,
397) -> Vec<omena_abstract_value::KLimitedCallSiteFlowInputV0> {
398    collect_expression_domain_flow_graphs(input)
399        .into_iter()
400        .map(|entry| {
401            let exit_node_id = expression_domain_flow_exit_node_id(&entry.graph);
402            omena_abstract_value::KLimitedCallSiteFlowInputV0 {
403                callee_key: "expression-domain-class-value".to_string(),
404                call_site_stack: vec![entry.file_path, entry.graph_id],
405                graph: entry.graph,
406                exit_node_id,
407            }
408        })
409        .collect()
410}
411
412fn reduced_product_axis_constraints_from_facts(
413    facts: &StringTypeFactsV2,
414) -> Vec<omena_abstract_value::AbstractClassValueV0> {
415    let mut constraints = Vec::new();
416
417    if let Some(prefix) = &facts.prefix {
418        constraints.push(omena_abstract_value::prefix_class_value(
419            prefix.clone(),
420            None,
421        ));
422    }
423
424    if let Some(suffix) = &facts.suffix {
425        constraints.push(omena_abstract_value::suffix_class_value(
426            suffix.clone(),
427            None,
428        ));
429    }
430
431    if facts.char_must.is_some()
432        || facts.char_may.is_some()
433        || facts.may_include_other_chars.is_some()
434    {
435        constraints.push(omena_abstract_value::char_inclusion_class_value(
436            facts.char_must.clone().unwrap_or_default(),
437            facts.char_may.clone().unwrap_or_default(),
438            None,
439            facts.may_include_other_chars.unwrap_or(false),
440        ));
441    }
442
443    constraints
444}
445
446fn expression_domain_flow_exit_node_id(
447    graph: &omena_abstract_value::ClassValueFlowGraphV0,
448) -> String {
449    if graph.nodes.iter().any(|node| node.id == "file-merge") {
450        "file-merge".to_string()
451    } else {
452        graph
453            .nodes
454            .first()
455            .map(|node| node.id.clone())
456            .unwrap_or_else(|| "exit".to_string())
457    }
458}
459
460fn expression_domain_control_flow_graph(
461    graph: &omena_abstract_value::ClassValueFlowGraphV0,
462) -> omena_abstract_value::ClassValueControlFlowGraphV0 {
463    let merge_node_id = "file-merge";
464    let has_merge = graph.nodes.iter().any(|node| node.id == merge_node_id);
465    let mut blocks = Vec::new();
466
467    if has_merge {
468        blocks.push(omena_abstract_value::ClassValueControlFlowBlockV0 {
469            id: "entry".to_string(),
470            nodes: Vec::new(),
471            successor_block_ids: graph
472                .nodes
473                .iter()
474                .filter(|node| node.id != merge_node_id)
475                .map(|node| format!("expr:{}", node.id))
476                .collect(),
477        });
478
479        for node in graph.nodes.iter().filter(|node| node.id != merge_node_id) {
480            blocks.push(omena_abstract_value::ClassValueControlFlowBlockV0 {
481                id: format!("expr:{}", node.id),
482                nodes: vec![node.clone()],
483                successor_block_ids: vec!["merge".to_string()],
484            });
485        }
486
487        if let Some(merge) = graph.nodes.iter().find(|node| node.id == merge_node_id) {
488            blocks.push(omena_abstract_value::ClassValueControlFlowBlockV0 {
489                id: "merge".to_string(),
490                nodes: vec![merge.clone()],
491                successor_block_ids: Vec::new(),
492            });
493        }
494    } else {
495        blocks.extend(graph.nodes.iter().map(|node| {
496            omena_abstract_value::ClassValueControlFlowBlockV0 {
497                id: format!("expr:{}", node.id),
498                nodes: vec![node.clone()],
499                successor_block_ids: Vec::new(),
500            }
501        }));
502    }
503
504    let entry_block_id = blocks
505        .first()
506        .map(|block| block.id.clone())
507        .unwrap_or_else(|| "entry".to_string());
508
509    omena_abstract_value::ClassValueControlFlowGraphV0 {
510        context_key: graph.context_key.clone(),
511        entry_block_id,
512        blocks,
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::{
519        collect_expression_domain_flow_graphs,
520        summarize_expression_domain_call_site_flow_analysis_input,
521        summarize_expression_domain_candidates_input,
522        summarize_expression_domain_canonical_candidate_bundle_input,
523        summarize_expression_domain_canonical_producer_signal_input,
524        summarize_expression_domain_control_flow_analysis_input,
525        summarize_expression_domain_evaluator_candidates_input,
526        summarize_expression_domain_flow_analysis_input,
527        summarize_expression_domain_fragments_input, summarize_expression_domain_plan_input,
528        summarize_expression_domain_provenance_explanations_input,
529        summarize_expression_domain_reduced_product_iteration_input,
530    };
531    use crate::{StringTypeFactsV2, TypeFactEntryV2, test_support::sample_input};
532    use omena_abstract_value::AbstractClassValueV0;
533
534    #[test]
535    fn summarizes_expression_domain_counts() {
536        let summary = summarize_expression_domain_plan_input(&sample_input());
537
538        assert_eq!(
539            summary.planned_expression_ids,
540            vec!["expr-1".to_string(), "expr-2".to_string()]
541        );
542        assert_eq!(summary.value_domain_kinds.get("constrained"), Some(&1));
543        assert_eq!(summary.value_domain_kinds.get("finiteSet"), Some(&1));
544        assert_eq!(summary.value_constraint_kinds.get("prefixSuffix"), Some(&1));
545        assert_eq!(summary.constraint_detail_counts.prefix_count, 1);
546        assert_eq!(summary.constraint_detail_counts.suffix_count, 1);
547        assert_eq!(summary.constraint_detail_counts.min_len_count, 1);
548        assert_eq!(summary.finite_value_count, 2);
549    }
550
551    #[test]
552    fn summarizes_expression_domain_fragments() {
553        let summary = summarize_expression_domain_fragments_input(&sample_input());
554
555        assert_eq!(summary.fragments.len(), 2);
556        let first = &summary.fragments[0];
557        assert_eq!(first.expression_id, "expr-1");
558        assert_eq!(first.file_path, "/tmp/App.tsx");
559        assert_eq!(first.value_domain_kind, "constrained");
560        assert_eq!(first.value_constraint_kind.as_deref(), Some("prefixSuffix"));
561        assert_eq!(first.value_prefix.as_deref(), Some("btn-"));
562        assert_eq!(first.value_suffix.as_deref(), Some("-active"));
563        assert_eq!(first.value_min_len, Some(10));
564        assert_eq!(first.finite_value_count, 0);
565
566        let second = &summary.fragments[1];
567        assert_eq!(second.expression_id, "expr-2");
568        assert_eq!(second.value_domain_kind, "finiteSet");
569        assert_eq!(second.finite_value_count, 2);
570    }
571
572    #[test]
573    fn summarizes_expression_domain_candidates() {
574        let summary = summarize_expression_domain_candidates_input(&sample_input());
575
576        assert_eq!(summary.candidates.len(), 2);
577        assert_eq!(summary.candidates[0].expression_id, "expr-1");
578        assert_eq!(summary.candidates[0].value_domain_kind, "constrained");
579        assert_eq!(
580            summary.candidates[0].value_constraint_kind.as_deref(),
581            Some("prefixSuffix")
582        );
583        assert_eq!(summary.candidates[1].expression_id, "expr-2");
584        assert_eq!(summary.candidates[1].finite_value_count, 2);
585    }
586
587    #[test]
588    fn summarizes_expression_domain_canonical_candidate_bundle() {
589        let summary = summarize_expression_domain_canonical_candidate_bundle_input(&sample_input());
590
591        assert_eq!(summary.plan_summary.planned_expression_ids.len(), 2);
592        assert_eq!(summary.fragments.len(), 2);
593        assert_eq!(summary.candidates.len(), 2);
594    }
595
596    #[test]
597    fn summarizes_expression_domain_evaluator_candidates() {
598        let summary = summarize_expression_domain_evaluator_candidates_input(&sample_input());
599
600        assert_eq!(summary.schema_version, "0");
601        assert_eq!(summary.input_version, "2");
602        assert_eq!(summary.results.len(), 2);
603        assert_eq!(summary.results[0].kind, "expression-domain");
604        assert_eq!(summary.results[0].query_id, "expr-1");
605        assert_eq!(summary.results[0].payload.value_domain_kind, "prefixSuffix");
606        assert_eq!(
607            summary.results[0].payload.value_constraint_kind.as_deref(),
608            Some("prefixSuffix")
609        );
610        assert_eq!(summary.results[1].payload.finite_value_count, 2);
611    }
612
613    #[test]
614    fn summarizes_expression_domain_provenance_explanations() {
615        let summary = summarize_expression_domain_provenance_explanations_input(&sample_input());
616
617        assert_eq!(summary.schema_version, "0");
618        assert_eq!(
619            summary.product,
620            "engine-input-producers.expression-domain-provenance-explanations"
621        );
622        assert_eq!(summary.input_version, "2");
623        assert_eq!(summary.explanation_count, 2);
624        assert_eq!(summary.explanations[0].expression_id, "expr-1");
625        assert_eq!(summary.explanations[0].input_fact_kind, "constrained");
626        assert_eq!(
627            summary.explanations[0].input_constraint_kind.as_deref(),
628            Some("prefixSuffix")
629        );
630        assert_eq!(summary.explanations[0].reduced_kind, "prefixSuffix");
631        assert_eq!(
632            summary.explanations[0].derivation.product,
633            "omena-abstract-value.reduced-class-value-derivation"
634        );
635        assert_eq!(
636            summary.explanations[0].provenance_tree.product,
637            "omena-abstract-value.provenance-tree"
638        );
639        assert_eq!(
640            summary.explanations[0].provenance_tree.root.operation,
641            "constraintDomain"
642        );
643    }
644
645    #[test]
646    fn expression_domain_evaluator_reports_reduced_value_domain_kind() {
647        let mut input = sample_input();
648        input.type_facts.push(TypeFactEntryV2 {
649            file_path: "/tmp/App.tsx".to_string(),
650            expression_id: "expr-3".to_string(),
651            facts: StringTypeFactsV2 {
652                kind: "finiteSet".to_string(),
653                constraint_kind: Some("prefix".to_string()),
654                values: Some(vec!["btn-active".to_string(), "card".to_string()]),
655                prefix: Some("btn-".to_string()),
656                suffix: None,
657                min_len: None,
658                max_len: None,
659                char_must: None,
660                char_may: None,
661                may_include_other_chars: None,
662            },
663        });
664
665        let fragments = summarize_expression_domain_fragments_input(&input);
666        let candidates = summarize_expression_domain_candidates_input(&input);
667        let evaluator_candidates = summarize_expression_domain_evaluator_candidates_input(&input);
668
669        assert_eq!(fragments.fragments[2].expression_id, "expr-3");
670        assert_eq!(fragments.fragments[2].value_domain_kind, "finiteSet");
671        assert_eq!(candidates.candidates[2].expression_id, "expr-3");
672        assert_eq!(candidates.candidates[2].value_domain_kind, "finiteSet");
673        assert_eq!(evaluator_candidates.results[2].query_id, "expr-3");
674        assert_eq!(
675            evaluator_candidates.results[2].payload.value_domain_kind,
676            "exact"
677        );
678        assert_eq!(
679            evaluator_candidates.results[2]
680                .payload
681                .value_domain_derivation
682                .reduced_kind,
683            "exact"
684        );
685        assert_eq!(
686            evaluator_candidates.results[2]
687                .payload
688                .value_domain_derivation
689                .steps[1]
690                .operation,
691            "intersectConstraint"
692        );
693        assert_eq!(
694            evaluator_candidates.results[2]
695                .payload
696                .value_domain_provenance_tree
697                .product,
698            "omena-abstract-value.provenance-tree"
699        );
700        assert_eq!(
701            evaluator_candidates.results[2]
702                .payload
703                .value_domain_provenance_tree
704                .root
705                .operation,
706            "exactLiteral"
707        );
708    }
709
710    #[test]
711    fn summarizes_expression_domain_flow_analysis() {
712        let mut input = sample_input();
713        input.type_facts = vec![
714            exact_type_fact("expr-branch-a", "btn-primary"),
715            exact_type_fact("expr-branch-b", "btn-secondary"),
716            exact_type_fact("expr-branch-c", "card"),
717        ];
718
719        let summary = summarize_expression_domain_flow_analysis_input(&input);
720
721        assert_eq!(summary.schema_version, "0");
722        assert_eq!(
723            summary.product,
724            "engine-input-producers.expression-domain-flow-analysis"
725        );
726        assert_eq!(summary.analyses.len(), 1);
727        assert_eq!(summary.analyses[0].file_path, "/tmp/App.tsx");
728        assert_eq!(summary.analyses[0].analysis.context_sensitivity, "1-cfa");
729        assert!(summary.analyses[0].analysis.converged);
730        assert_eq!(
731            summary.analyses[0]
732                .analysis
733                .nodes
734                .iter()
735                .find(|node| node.id == "file-merge")
736                .map(|node| (node.value_kind, &node.value)),
737            Some((
738                "finiteSet",
739                &AbstractClassValueV0::FiniteSet {
740                    values: vec![
741                        "btn-primary".to_string(),
742                        "btn-secondary".to_string(),
743                        "card".to_string(),
744                    ]
745                }
746            ))
747        );
748    }
749
750    #[test]
751    fn exposes_expression_domain_flow_graphs_for_query_runtime_reuse() {
752        let mut input = sample_input();
753        input.type_facts = vec![
754            exact_type_fact("expr-branch-a", "btn-primary"),
755            exact_type_fact("expr-branch-b", "btn-secondary"),
756        ];
757
758        let graphs = collect_expression_domain_flow_graphs(&input);
759
760        assert_eq!(graphs.len(), 1);
761        assert_eq!(graphs[0].graph_id, "/tmp/App.tsx:expression-domain-flow");
762        assert_eq!(
763            graphs[0].graph.context_key.as_deref(),
764            Some(graphs[0].graph_id.as_str())
765        );
766        assert!(
767            graphs[0]
768                .graph
769                .nodes
770                .iter()
771                .any(|node| node.id == "file-merge")
772        );
773    }
774
775    #[test]
776    fn summarizes_expression_domain_control_flow_analysis() {
777        let mut input = sample_input();
778        input.type_facts = vec![
779            exact_type_fact("expr-branch-a", "btn-primary"),
780            exact_type_fact("expr-branch-b", "btn-secondary"),
781        ];
782
783        let summary = summarize_expression_domain_control_flow_analysis_input(&input);
784
785        assert_eq!(
786            summary.product,
787            "engine-input-producers.expression-domain-control-flow-analysis"
788        );
789        assert_eq!(summary.analyses.len(), 1);
790        assert_eq!(summary.analyses[0].analysis.block_count, 4);
791        assert_eq!(summary.analyses[0].analysis.edge_count, 4);
792        assert_eq!(
793            summary.analyses[0].analysis.branch_block_ids,
794            vec!["entry".to_string()]
795        );
796        assert_eq!(
797            summary.analyses[0].analysis.join_block_ids,
798            vec!["merge".to_string()]
799        );
800        assert_eq!(
801            summary.analyses[0].analysis.flow_analysis.product,
802            "omena-abstract-value.flow-analysis"
803        );
804        assert!(
805            summary.analyses[0]
806                .analysis
807                .unreachable_block_ids
808                .is_empty()
809        );
810    }
811
812    #[test]
813    fn summarizes_expression_domain_call_site_flow_analysis_for_zero_and_one_cfa() {
814        let mut input = sample_input();
815        input.type_facts = vec![
816            exact_type_fact_in_file("/tmp/App.tsx", "expr-primary", "btn-primary"),
817            exact_type_fact_in_file("/tmp/Card.tsx", "expr-secondary", "btn-secondary"),
818        ];
819
820        let summary = summarize_expression_domain_call_site_flow_analysis_input(&input);
821
822        assert_eq!(summary.schema_version, "0");
823        assert_eq!(
824            summary.product,
825            "engine-input-producers.expression-domain-call-site-flow-analysis"
826        );
827        assert_eq!(summary.zero_cfa.context_sensitivity, "0-cfa");
828        assert_eq!(summary.one_cfa.context_sensitivity, "1-cfa");
829        assert_eq!(summary.zero_cfa.call_site_count, 2);
830        assert_eq!(summary.one_cfa.call_site_count, 2);
831        assert_eq!(
832            summary.zero_cfa.entries[0].context_key,
833            "expression-domain-class-value@<root>"
834        );
835        assert_eq!(
836            summary.zero_cfa.entries[1].context_key,
837            "expression-domain-class-value@<root>"
838        );
839        assert_ne!(
840            summary.one_cfa.entries[0].context_key,
841            summary.one_cfa.entries[1].context_key
842        );
843        assert_eq!(
844            summary.zero_cfa.entries[0].exit_value,
845            AbstractClassValueV0::FiniteSet {
846                values: vec!["btn-primary".to_string(), "btn-secondary".to_string()]
847            }
848        );
849        assert_eq!(
850            summary.zero_cfa.entries[1].exit_value,
851            summary.zero_cfa.entries[0].exit_value
852        );
853        assert_eq!(
854            summary.one_cfa.entries[0].exit_value,
855            AbstractClassValueV0::Exact {
856                value: "btn-primary".to_string()
857            }
858        );
859        assert_eq!(
860            summary.one_cfa.entries[1].exit_value,
861            AbstractClassValueV0::Exact {
862                value: "btn-secondary".to_string()
863            }
864        );
865    }
866
867    #[test]
868    fn summarizes_expression_domain_reduced_product_iteration() {
869        let mut input = sample_input();
870        input.type_facts = vec![TypeFactEntryV2 {
871            file_path: "/tmp/App.tsx".to_string(),
872            expression_id: "expr-reduced".to_string(),
873            facts: StringTypeFactsV2 {
874                kind: "constrained".to_string(),
875                constraint_kind: Some("composite".to_string()),
876                values: None,
877                prefix: Some("btn-".to_string()),
878                suffix: Some("-active".to_string()),
879                min_len: None,
880                max_len: None,
881                char_must: Some("a".to_string()),
882                char_may: Some("-abceintv".to_string()),
883                may_include_other_chars: Some(false),
884            },
885        }];
886
887        let summary = summarize_expression_domain_reduced_product_iteration_input(&input);
888
889        assert_eq!(summary.schema_version, "0");
890        assert_eq!(
891            summary.product,
892            "engine-input-producers.expression-domain-reduced-product-iteration"
893        );
894        assert_eq!(summary.input_version, "2");
895        assert_eq!(summary.iteration_count, 1);
896        assert_eq!(summary.iterations[0].expression_id, "expr-reduced");
897        assert_eq!(summary.iterations[0].axis_constraint_count, 3);
898        assert_eq!(summary.iterations[0].input_value_kind, "composite");
899        assert_eq!(summary.iterations[0].iteration.input_count, 3);
900        assert_eq!(summary.iterations[0].iteration.applied_constraint_count, 3);
901        assert!(summary.iterations[0].iteration.converged);
902        assert!(summary.iterations[0].iteration.monotone_witness_valid);
903        assert_eq!(summary.iterations[0].iteration.result_kind, "composite");
904    }
905
906    #[test]
907    fn summarizes_expression_domain_canonical_producer_signal() {
908        let summary = summarize_expression_domain_canonical_producer_signal_input(&sample_input());
909
910        assert_eq!(summary.schema_version, "0");
911        assert_eq!(summary.input_version, "2");
912        assert_eq!(
913            summary
914                .canonical_bundle
915                .plan_summary
916                .planned_expression_ids
917                .len(),
918            2
919        );
920        assert_eq!(summary.canonical_bundle.fragments.len(), 2);
921        assert_eq!(summary.canonical_bundle.candidates.len(), 2);
922        assert_eq!(summary.evaluator_candidates.results.len(), 2);
923    }
924
925    fn exact_type_fact(expression_id: &str, value: &str) -> TypeFactEntryV2 {
926        exact_type_fact_in_file("/tmp/App.tsx", expression_id, value)
927    }
928
929    fn exact_type_fact_in_file(
930        file_path: &str,
931        expression_id: &str,
932        value: &str,
933    ) -> TypeFactEntryV2 {
934        TypeFactEntryV2 {
935            file_path: file_path.to_string(),
936            expression_id: expression_id.to_string(),
937            facts: StringTypeFactsV2 {
938                kind: "exact".to_string(),
939                constraint_kind: None,
940                values: Some(vec![value.to_string()]),
941                prefix: None,
942                suffix: None,
943                min_len: None,
944                max_len: None,
945                char_must: None,
946                char_may: None,
947                may_include_other_chars: None,
948            },
949        }
950    }
951}