Skip to main content

engine_input_producers/
expression_domain.rs

1use std::collections::BTreeMap;
2
3use crate::{
4    ConstraintDetailCounts, ConstraintDetailInput, EngineInputV2, ExpressionDomainCandidateV0,
5    ExpressionDomainCandidatesV0, ExpressionDomainCanonicalCandidateBundleV0,
6    ExpressionDomainCanonicalProducerSignalV0, ExpressionDomainEvaluatorCandidatePayloadV0,
7    ExpressionDomainEvaluatorCandidateV0, ExpressionDomainEvaluatorCandidatesV0,
8    ExpressionDomainFlowAnalysisEntryV0, ExpressionDomainFlowAnalysisV0,
9    ExpressionDomainFragmentV0, ExpressionDomainFragmentsV0, ExpressionDomainPlanSummaryV0,
10    TypeFactEntryV2, abstract_value_facts, collect_constraint_detail_counts,
11    map_reduced_expression_value_domain_derivation, map_reduced_expression_value_domain_kind,
12};
13
14struct ExpressionDomainInputRows {
15    plan_summary: ExpressionDomainPlanSummaryV0,
16    fragments: Vec<ExpressionDomainFragmentV0>,
17    candidates: Vec<ExpressionDomainCandidateV0>,
18    evaluator_candidates: Vec<ExpressionDomainEvaluatorCandidateV0>,
19}
20
21fn collect_expression_domain_input_rows(input: &EngineInputV2) -> ExpressionDomainInputRows {
22    let mut planned_expression_ids = Vec::new();
23    let mut value_domain_kinds = BTreeMap::new();
24    let mut value_constraint_kinds = BTreeMap::new();
25    let mut constraint_detail_counts = ConstraintDetailCounts::default();
26    let mut finite_value_count = 0usize;
27    let mut fragments = Vec::new();
28    let mut candidates = Vec::new();
29    let mut evaluator_candidates = Vec::new();
30
31    for entry in &input.type_facts {
32        planned_expression_ids.push(entry.expression_id.clone());
33        *value_domain_kinds
34            .entry(entry.facts.kind.clone())
35            .or_insert(0) += 1;
36
37        if let Some(values) = &entry.facts.values {
38            finite_value_count += values.len();
39        }
40
41        if let Some(constraint_kind) = &entry.facts.constraint_kind {
42            *value_constraint_kinds
43                .entry(constraint_kind.clone())
44                .or_insert(0) += 1;
45        }
46
47        collect_constraint_detail_counts(
48            &mut constraint_detail_counts,
49            ConstraintDetailInput {
50                prefix: entry.facts.prefix.as_ref(),
51                suffix: entry.facts.suffix.as_ref(),
52                min_len: entry.facts.min_len,
53                max_len: entry.facts.max_len,
54                char_must: entry.facts.char_must.as_ref(),
55                char_may: entry.facts.char_may.as_ref(),
56                may_include_other_chars: entry.facts.may_include_other_chars,
57            },
58        );
59
60        let fragment = ExpressionDomainFragmentV0 {
61            expression_id: entry.expression_id.clone(),
62            file_path: entry.file_path.clone(),
63            value_domain_kind: entry.facts.kind.clone(),
64            value_constraint_kind: entry.facts.constraint_kind.clone(),
65            value_prefix: entry.facts.prefix.clone(),
66            value_suffix: entry.facts.suffix.clone(),
67            value_min_len: entry.facts.min_len,
68            value_max_len: entry.facts.max_len,
69            value_char_must: entry.facts.char_must.clone(),
70            value_char_may: entry.facts.char_may.clone(),
71            value_may_include_other_chars: entry.facts.may_include_other_chars,
72            finite_value_count: entry.facts.values.as_ref().map_or(0, Vec::len),
73        };
74        fragments.push(fragment.clone());
75        candidates.push(ExpressionDomainCandidateV0 {
76            expression_id: fragment.expression_id,
77            file_path: fragment.file_path,
78            value_domain_kind: fragment.value_domain_kind,
79            value_constraint_kind: fragment.value_constraint_kind,
80            value_prefix: fragment.value_prefix,
81            value_suffix: fragment.value_suffix,
82            value_min_len: fragment.value_min_len,
83            value_max_len: fragment.value_max_len,
84            value_char_must: fragment.value_char_must,
85            value_char_may: fragment.value_char_may,
86            value_may_include_other_chars: fragment.value_may_include_other_chars,
87            finite_value_count: fragment.finite_value_count,
88        });
89
90        evaluator_candidates.push(ExpressionDomainEvaluatorCandidateV0 {
91            kind: "expression-domain",
92            file_path: entry.file_path.clone(),
93            query_id: entry.expression_id.clone(),
94            payload: ExpressionDomainEvaluatorCandidatePayloadV0 {
95                expression_id: entry.expression_id.clone(),
96                value_domain_kind: map_reduced_expression_value_domain_kind(&entry.facts),
97                value_constraint_kind: entry.facts.constraint_kind.clone(),
98                value_prefix: entry.facts.prefix.clone(),
99                value_suffix: entry.facts.suffix.clone(),
100                value_min_len: entry.facts.min_len,
101                value_max_len: entry.facts.max_len,
102                value_char_must: entry.facts.char_must.clone(),
103                value_char_may: entry.facts.char_may.clone(),
104                value_may_include_other_chars: entry.facts.may_include_other_chars,
105                finite_value_count: entry.facts.values.as_ref().map_or(0, Vec::len),
106                value_domain_derivation: map_reduced_expression_value_domain_derivation(
107                    &entry.facts,
108                ),
109            },
110        });
111    }
112
113    fragments.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
114    candidates.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
115    evaluator_candidates.sort_by(|a, b| a.query_id.cmp(&b.query_id));
116
117    ExpressionDomainInputRows {
118        plan_summary: ExpressionDomainPlanSummaryV0 {
119            schema_version: "0",
120            input_version: input.version.clone(),
121            planned_expression_ids,
122            value_domain_kinds,
123            value_constraint_kinds,
124            constraint_detail_counts,
125            finite_value_count,
126        },
127        fragments,
128        candidates,
129        evaluator_candidates,
130    }
131}
132
133pub fn summarize_expression_domain_plan_input(
134    input: &EngineInputV2,
135) -> ExpressionDomainPlanSummaryV0 {
136    collect_expression_domain_input_rows(input).plan_summary
137}
138
139pub fn summarize_expression_domain_fragments_input(
140    input: &EngineInputV2,
141) -> ExpressionDomainFragmentsV0 {
142    let rows = collect_expression_domain_input_rows(input);
143
144    ExpressionDomainFragmentsV0 {
145        schema_version: "0",
146        input_version: input.version.clone(),
147        fragments: rows.fragments,
148    }
149}
150
151pub fn summarize_expression_domain_candidates_input(
152    input: &EngineInputV2,
153) -> ExpressionDomainCandidatesV0 {
154    let rows = collect_expression_domain_input_rows(input);
155
156    ExpressionDomainCandidatesV0 {
157        schema_version: "0",
158        input_version: input.version.clone(),
159        candidates: rows.candidates,
160    }
161}
162
163pub fn summarize_expression_domain_canonical_candidate_bundle_input(
164    input: &EngineInputV2,
165) -> ExpressionDomainCanonicalCandidateBundleV0 {
166    let rows = collect_expression_domain_input_rows(input);
167
168    ExpressionDomainCanonicalCandidateBundleV0 {
169        schema_version: "0",
170        input_version: input.version.clone(),
171        plan_summary: rows.plan_summary,
172        fragments: rows.fragments,
173        candidates: rows.candidates,
174    }
175}
176
177pub fn summarize_expression_domain_evaluator_candidates_input(
178    input: &EngineInputV2,
179) -> ExpressionDomainEvaluatorCandidatesV0 {
180    let rows = collect_expression_domain_input_rows(input);
181
182    ExpressionDomainEvaluatorCandidatesV0 {
183        schema_version: "0",
184        input_version: input.version.clone(),
185        results: rows.evaluator_candidates,
186    }
187}
188
189pub fn summarize_expression_domain_canonical_producer_signal_input(
190    input: &EngineInputV2,
191) -> ExpressionDomainCanonicalProducerSignalV0 {
192    let rows = collect_expression_domain_input_rows(input);
193    let input_version = input.version.clone();
194
195    ExpressionDomainCanonicalProducerSignalV0 {
196        schema_version: "0",
197        input_version: input_version.clone(),
198        canonical_bundle: ExpressionDomainCanonicalCandidateBundleV0 {
199            schema_version: "0",
200            input_version: input_version.clone(),
201            plan_summary: rows.plan_summary,
202            fragments: rows.fragments,
203            candidates: rows.candidates,
204        },
205        evaluator_candidates: ExpressionDomainEvaluatorCandidatesV0 {
206            schema_version: "0",
207            input_version,
208            results: rows.evaluator_candidates,
209        },
210    }
211}
212
213pub fn summarize_expression_domain_flow_analysis_input(
214    input: &EngineInputV2,
215) -> ExpressionDomainFlowAnalysisV0 {
216    let mut by_file = BTreeMap::<String, Vec<&TypeFactEntryV2>>::new();
217    for entry in &input.type_facts {
218        by_file
219            .entry(entry.file_path.clone())
220            .or_default()
221            .push(entry);
222    }
223
224    let analyses = by_file
225        .into_iter()
226        .map(|(file_path, mut entries)| {
227            entries.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
228            let graph_id = format!("{file_path}:expression-domain-flow");
229            let mut nodes = entries
230                .iter()
231                .map(|entry| omena_abstract_value::ClassValueFlowNodeV0 {
232                    id: entry.expression_id.clone(),
233                    predecessors: Vec::new(),
234                    transfer: omena_abstract_value::ClassValueFlowTransferV0::AssignFacts(
235                        abstract_value_facts(&entry.facts),
236                    ),
237                })
238                .collect::<Vec<_>>();
239
240            if entries.len() > 1 {
241                nodes.push(omena_abstract_value::ClassValueFlowNodeV0 {
242                    id: "file-merge".to_string(),
243                    predecessors: entries
244                        .iter()
245                        .map(|entry| entry.expression_id.clone())
246                        .collect(),
247                    transfer: omena_abstract_value::ClassValueFlowTransferV0::Join,
248                });
249            }
250
251            let graph = omena_abstract_value::ClassValueFlowGraphV0 {
252                context_key: Some(graph_id.clone()),
253                nodes,
254            };
255
256            ExpressionDomainFlowAnalysisEntryV0 {
257                graph_id,
258                file_path,
259                analysis: omena_abstract_value::analyze_class_value_flow(&graph),
260            }
261        })
262        .collect();
263
264    ExpressionDomainFlowAnalysisV0 {
265        schema_version: "0",
266        product: "engine-input-producers.expression-domain-flow-analysis",
267        input_version: input.version.clone(),
268        analyses,
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::{
275        summarize_expression_domain_candidates_input,
276        summarize_expression_domain_canonical_candidate_bundle_input,
277        summarize_expression_domain_canonical_producer_signal_input,
278        summarize_expression_domain_evaluator_candidates_input,
279        summarize_expression_domain_flow_analysis_input,
280        summarize_expression_domain_fragments_input, summarize_expression_domain_plan_input,
281    };
282    use crate::{StringTypeFactsV2, TypeFactEntryV2, test_support::sample_input};
283    use omena_abstract_value::AbstractClassValueV0;
284
285    #[test]
286    fn summarizes_expression_domain_counts() {
287        let summary = summarize_expression_domain_plan_input(&sample_input());
288
289        assert_eq!(
290            summary.planned_expression_ids,
291            vec!["expr-1".to_string(), "expr-2".to_string()]
292        );
293        assert_eq!(summary.value_domain_kinds.get("constrained"), Some(&1));
294        assert_eq!(summary.value_domain_kinds.get("finiteSet"), Some(&1));
295        assert_eq!(summary.value_constraint_kinds.get("prefixSuffix"), Some(&1));
296        assert_eq!(summary.constraint_detail_counts.prefix_count, 1);
297        assert_eq!(summary.constraint_detail_counts.suffix_count, 1);
298        assert_eq!(summary.constraint_detail_counts.min_len_count, 1);
299        assert_eq!(summary.finite_value_count, 2);
300    }
301
302    #[test]
303    fn summarizes_expression_domain_fragments() {
304        let summary = summarize_expression_domain_fragments_input(&sample_input());
305
306        assert_eq!(summary.fragments.len(), 2);
307        let first = &summary.fragments[0];
308        assert_eq!(first.expression_id, "expr-1");
309        assert_eq!(first.file_path, "/tmp/App.tsx");
310        assert_eq!(first.value_domain_kind, "constrained");
311        assert_eq!(first.value_constraint_kind.as_deref(), Some("prefixSuffix"));
312        assert_eq!(first.value_prefix.as_deref(), Some("btn-"));
313        assert_eq!(first.value_suffix.as_deref(), Some("-active"));
314        assert_eq!(first.value_min_len, Some(10));
315        assert_eq!(first.finite_value_count, 0);
316
317        let second = &summary.fragments[1];
318        assert_eq!(second.expression_id, "expr-2");
319        assert_eq!(second.value_domain_kind, "finiteSet");
320        assert_eq!(second.finite_value_count, 2);
321    }
322
323    #[test]
324    fn summarizes_expression_domain_candidates() {
325        let summary = summarize_expression_domain_candidates_input(&sample_input());
326
327        assert_eq!(summary.candidates.len(), 2);
328        assert_eq!(summary.candidates[0].expression_id, "expr-1");
329        assert_eq!(summary.candidates[0].value_domain_kind, "constrained");
330        assert_eq!(
331            summary.candidates[0].value_constraint_kind.as_deref(),
332            Some("prefixSuffix")
333        );
334        assert_eq!(summary.candidates[1].expression_id, "expr-2");
335        assert_eq!(summary.candidates[1].finite_value_count, 2);
336    }
337
338    #[test]
339    fn summarizes_expression_domain_canonical_candidate_bundle() {
340        let summary = summarize_expression_domain_canonical_candidate_bundle_input(&sample_input());
341
342        assert_eq!(summary.plan_summary.planned_expression_ids.len(), 2);
343        assert_eq!(summary.fragments.len(), 2);
344        assert_eq!(summary.candidates.len(), 2);
345    }
346
347    #[test]
348    fn summarizes_expression_domain_evaluator_candidates() {
349        let summary = summarize_expression_domain_evaluator_candidates_input(&sample_input());
350
351        assert_eq!(summary.schema_version, "0");
352        assert_eq!(summary.input_version, "2");
353        assert_eq!(summary.results.len(), 2);
354        assert_eq!(summary.results[0].kind, "expression-domain");
355        assert_eq!(summary.results[0].query_id, "expr-1");
356        assert_eq!(summary.results[0].payload.value_domain_kind, "prefixSuffix");
357        assert_eq!(
358            summary.results[0].payload.value_constraint_kind.as_deref(),
359            Some("prefixSuffix")
360        );
361        assert_eq!(summary.results[1].payload.finite_value_count, 2);
362    }
363
364    #[test]
365    fn expression_domain_evaluator_reports_reduced_value_domain_kind() {
366        let mut input = sample_input();
367        input.type_facts.push(TypeFactEntryV2 {
368            file_path: "/tmp/App.tsx".to_string(),
369            expression_id: "expr-3".to_string(),
370            facts: StringTypeFactsV2 {
371                kind: "finiteSet".to_string(),
372                constraint_kind: Some("prefix".to_string()),
373                values: Some(vec!["btn-active".to_string(), "card".to_string()]),
374                prefix: Some("btn-".to_string()),
375                suffix: None,
376                min_len: None,
377                max_len: None,
378                char_must: None,
379                char_may: None,
380                may_include_other_chars: None,
381            },
382        });
383
384        let fragments = summarize_expression_domain_fragments_input(&input);
385        let candidates = summarize_expression_domain_candidates_input(&input);
386        let evaluator_candidates = summarize_expression_domain_evaluator_candidates_input(&input);
387
388        assert_eq!(fragments.fragments[2].expression_id, "expr-3");
389        assert_eq!(fragments.fragments[2].value_domain_kind, "finiteSet");
390        assert_eq!(candidates.candidates[2].expression_id, "expr-3");
391        assert_eq!(candidates.candidates[2].value_domain_kind, "finiteSet");
392        assert_eq!(evaluator_candidates.results[2].query_id, "expr-3");
393        assert_eq!(
394            evaluator_candidates.results[2].payload.value_domain_kind,
395            "exact"
396        );
397        assert_eq!(
398            evaluator_candidates.results[2]
399                .payload
400                .value_domain_derivation
401                .reduced_kind,
402            "exact"
403        );
404        assert_eq!(
405            evaluator_candidates.results[2]
406                .payload
407                .value_domain_derivation
408                .steps[1]
409                .operation,
410            "intersectConstraint"
411        );
412    }
413
414    #[test]
415    fn summarizes_expression_domain_flow_analysis() {
416        let mut input = sample_input();
417        input.type_facts = vec![
418            exact_type_fact("expr-branch-a", "btn-primary"),
419            exact_type_fact("expr-branch-b", "btn-secondary"),
420            exact_type_fact("expr-branch-c", "card"),
421        ];
422
423        let summary = summarize_expression_domain_flow_analysis_input(&input);
424
425        assert_eq!(summary.schema_version, "0");
426        assert_eq!(
427            summary.product,
428            "engine-input-producers.expression-domain-flow-analysis"
429        );
430        assert_eq!(summary.analyses.len(), 1);
431        assert_eq!(summary.analyses[0].file_path, "/tmp/App.tsx");
432        assert_eq!(summary.analyses[0].analysis.context_sensitivity, "1-cfa");
433        assert!(summary.analyses[0].analysis.converged);
434        assert_eq!(
435            summary.analyses[0]
436                .analysis
437                .nodes
438                .iter()
439                .find(|node| node.id == "file-merge")
440                .map(|node| (node.value_kind, &node.value)),
441            Some((
442                "finiteSet",
443                &AbstractClassValueV0::FiniteSet {
444                    values: vec![
445                        "btn-primary".to_string(),
446                        "btn-secondary".to_string(),
447                        "card".to_string(),
448                    ]
449                }
450            ))
451        );
452    }
453
454    #[test]
455    fn summarizes_expression_domain_canonical_producer_signal() {
456        let summary = summarize_expression_domain_canonical_producer_signal_input(&sample_input());
457
458        assert_eq!(summary.schema_version, "0");
459        assert_eq!(summary.input_version, "2");
460        assert_eq!(
461            summary
462                .canonical_bundle
463                .plan_summary
464                .planned_expression_ids
465                .len(),
466            2
467        );
468        assert_eq!(summary.canonical_bundle.fragments.len(), 2);
469        assert_eq!(summary.canonical_bundle.candidates.len(), 2);
470        assert_eq!(summary.evaluator_candidates.results.len(), 2);
471    }
472
473    fn exact_type_fact(expression_id: &str, value: &str) -> TypeFactEntryV2 {
474        TypeFactEntryV2 {
475            file_path: "/tmp/App.tsx".to_string(),
476            expression_id: expression_id.to_string(),
477            facts: StringTypeFactsV2 {
478                kind: "exact".to_string(),
479                constraint_kind: None,
480                values: Some(vec![value.to_string()]),
481                prefix: None,
482                suffix: None,
483                min_len: None,
484                max_len: None,
485                char_must: None,
486                char_may: None,
487                may_include_other_chars: None,
488            },
489        }
490    }
491}