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                    boundary_effect: omena_abstract_value::ClassBoundaryEffectV0::UnknownBoundary,
357                    transfer: omena_abstract_value::ClassValueFlowTransferV0::AssignFacts(
358                        abstract_value_facts(&entry.facts),
359                    ),
360                }],
361            };
362
363            ExpressionDomainFlowGraphEntryV0 {
364                graph_id,
365                file_path: entry.file_path.clone(),
366                graph,
367            }
368        })
369        .collect()
370}
371
372#[derive(Debug, Clone, PartialEq, Eq)]
373struct ExpressionDomainControlFlowGraphEntryV0 {
374    graph_id: String,
375    file_path: String,
376    expression_id: String,
377    graph: omena_abstract_value::ClassValueControlFlowGraphV0,
378}
379
380#[derive(Debug, Clone, PartialEq, Eq)]
381pub(crate) struct ExpressionDomainSelectorCertaintyFlowHedgeV0 {
382    pub(crate) graph_converged: bool,
383    pub(crate) contains_flow_iteration_limit: bool,
384}
385
386pub(crate) type ExpressionDomainSelectorCertaintyFlowHedgesV0 =
387    BTreeMap<(String, String), ExpressionDomainSelectorCertaintyFlowHedgeV0>;
388
389pub(crate) fn collect_expression_domain_selector_certainty_flow_hedges(
390    input: &EngineInputV2,
391) -> ExpressionDomainSelectorCertaintyFlowHedgesV0 {
392    let mut hedges = ExpressionDomainSelectorCertaintyFlowHedgesV0::new();
393    for entry in collect_expression_domain_control_flow_graphs(input) {
394        let analysis = omena_abstract_value::analyze_class_value_control_flow_graph(&entry.graph);
395        let contains_flow_iteration_limit = analysis
396            .flow_analysis
397            .nodes
398            .iter()
399            .any(|node| abstract_value_contains_flow_iteration_limit(&node.value));
400        let key = (entry.file_path, entry.expression_id);
401        let previous = hedges.insert(
402            key.clone(),
403            ExpressionDomainSelectorCertaintyFlowHedgeV0 {
404                graph_converged: analysis.flow_analysis.converged,
405                contains_flow_iteration_limit,
406            },
407        );
408        assert!(
409            previous.is_none(),
410            "selector-certainty flow hedge duplicate type-fact key: file_path={:?} expression_id={:?}",
411            key.0,
412            key.1
413        );
414    }
415
416    hedges
417}
418
419fn abstract_value_contains_flow_iteration_limit(
420    value: &omena_abstract_value::AbstractClassValueV0,
421) -> bool {
422    use omena_abstract_value::{AbstractClassValueProvenanceV0, AbstractClassValueV0};
423
424    let provenance = match value {
425        AbstractClassValueV0::Automaton { provenance, .. }
426        | AbstractClassValueV0::Prefix { provenance, .. }
427        | AbstractClassValueV0::Suffix { provenance, .. }
428        | AbstractClassValueV0::PrefixSuffix { provenance, .. }
429        | AbstractClassValueV0::CharInclusion { provenance, .. }
430        | AbstractClassValueV0::Composite { provenance, .. }
431        | AbstractClassValueV0::Top { provenance } => *provenance,
432        AbstractClassValueV0::Bottom
433        | AbstractClassValueV0::Exact { .. }
434        | AbstractClassValueV0::FiniteSet { .. } => None,
435    };
436
437    provenance == Some(AbstractClassValueProvenanceV0::FlowIterationLimit)
438}
439
440fn collect_expression_domain_control_flow_graphs(
441    input: &EngineInputV2,
442) -> Vec<ExpressionDomainControlFlowGraphEntryV0> {
443    input
444        .type_facts
445        .iter()
446        .filter_map(|entry| {
447            entry.control_flow_graph.as_ref().map(|graph| {
448                let graph_id = format!(
449                    "{}:{}:expression-domain-control-flow",
450                    entry.file_path, entry.expression_id
451                );
452                ExpressionDomainControlFlowGraphEntryV0 {
453                    graph_id: graph_id.clone(),
454                    file_path: entry.file_path.clone(),
455                    expression_id: entry.expression_id.clone(),
456                    graph: expression_domain_control_flow_graph_from_type_fact_graph(
457                        &graph_id, entry, graph,
458                    ),
459                }
460            })
461        })
462        .collect()
463}
464
465fn collect_expression_domain_call_site_flow_inputs(
466    input: &EngineInputV2,
467) -> Vec<omena_abstract_value::KLimitedCallSiteFlowInputV0> {
468    collect_expression_domain_flow_graphs(input)
469        .into_iter()
470        .map(|entry| {
471            let exit_node_id = expression_domain_flow_exit_node_id(&entry.graph);
472            omena_abstract_value::KLimitedCallSiteFlowInputV0 {
473                callee_key: "expression-domain-class-value".to_string(),
474                call_site_stack: vec![entry.file_path, entry.graph_id],
475                graph: entry.graph,
476                exit_node_id,
477            }
478        })
479        .collect()
480}
481
482fn reduced_product_axis_constraints_from_facts(
483    facts: &StringTypeFactsV2,
484) -> Vec<omena_abstract_value::AbstractClassValueV0> {
485    let mut constraints = Vec::new();
486
487    if let Some(prefix) = &facts.prefix {
488        constraints.push(omena_abstract_value::prefix_class_value(
489            prefix.clone(),
490            None,
491        ));
492    }
493
494    if let Some(suffix) = &facts.suffix {
495        constraints.push(omena_abstract_value::suffix_class_value(
496            suffix.clone(),
497            None,
498        ));
499    }
500
501    if facts.char_must.is_some()
502        || facts.char_may.is_some()
503        || facts.may_include_other_chars.is_some()
504    {
505        constraints.push(omena_abstract_value::char_inclusion_class_value(
506            facts.char_must.clone().unwrap_or_default(),
507            facts.char_may.clone().unwrap_or_default(),
508            None,
509            facts.may_include_other_chars.unwrap_or(false),
510        ));
511    }
512
513    constraints
514}
515
516fn expression_domain_flow_exit_node_id(
517    graph: &omena_abstract_value::ClassValueFlowGraphV0,
518) -> String {
519    graph
520        .nodes
521        .first()
522        .map(|node| node.id.clone())
523        .unwrap_or_else(|| "exit".to_string())
524}
525
526fn expression_domain_control_flow_graph_from_type_fact_graph(
527    graph_id: &str,
528    entry: &TypeFactEntryV2,
529    graph: &TypeFactControlFlowGraphV2,
530) -> omena_abstract_value::ClassValueControlFlowGraphV0 {
531    let predecessor_block_ids = control_flow_predecessor_block_ids(&graph.blocks);
532    let block_node_ids = graph
533        .blocks
534        .iter()
535        .map(|block| {
536            (
537                block.id.clone(),
538                type_fact_control_flow_node_id(entry, block),
539            )
540        })
541        .collect::<BTreeMap<_, _>>();
542    let blocks = graph
543        .blocks
544        .iter()
545        .map(|block| {
546            let node_id = block_node_ids
547                .get(&block.id)
548                .cloned()
549                .unwrap_or_else(|| type_fact_control_flow_node_id(entry, block));
550            let predecessors = predecessor_block_ids
551                .get(&block.id)
552                .into_iter()
553                .flat_map(|ids| ids.iter())
554                .filter_map(|id| block_node_ids.get(id).cloned())
555                .collect::<Vec<_>>();
556            omena_abstract_value::ClassValueControlFlowBlockV0 {
557                id: block.id.clone(),
558                nodes: vec![omena_abstract_value::ClassValueFlowNodeV0 {
559                    id: node_id,
560                    predecessors,
561                    boundary_effect: type_fact_class_boundary_effect(block),
562                    transfer: type_fact_control_flow_transfer(block, &entry.facts),
563                }],
564                successor_block_ids: block.successor_block_ids.clone(),
565            }
566        })
567        .collect();
568
569    omena_abstract_value::ClassValueControlFlowGraphV0 {
570        context_key: Some(graph_id.to_string()),
571        entry_block_id: graph.entry_block_id.clone(),
572        blocks,
573    }
574}
575
576fn control_flow_predecessor_block_ids(
577    blocks: &[TypeFactControlFlowBlockV2],
578) -> BTreeMap<String, Vec<String>> {
579    let mut predecessors = BTreeMap::<String, Vec<String>>::new();
580    for block in blocks {
581        for successor in &block.successor_block_ids {
582            predecessors
583                .entry(successor.clone())
584                .or_default()
585                .push(block.id.clone());
586        }
587    }
588    predecessors
589}
590
591fn type_fact_control_flow_node_id(
592    entry: &TypeFactEntryV2,
593    block: &TypeFactControlFlowBlockV2,
594) -> String {
595    format!("{}:{}", entry.expression_id, block.id)
596}
597
598fn type_fact_control_flow_transfer(
599    block: &TypeFactControlFlowBlockV2,
600    facts: &StringTypeFactsV2,
601) -> omena_abstract_value::ClassValueFlowTransferV0 {
602    if let Some(block_facts) = &block.facts
603        && matches!(block.transfer_kind.as_str(), "assignFacts" | "concatFacts")
604    {
605        return omena_abstract_value::ClassValueFlowTransferV0::AssignFacts(abstract_value_facts(
606            block_facts,
607        ));
608    }
609
610    match block.transfer_kind.as_str() {
611        "assignFacts" => {
612            omena_abstract_value::ClassValueFlowTransferV0::AssignFacts(abstract_value_facts(facts))
613        }
614        "concatFacts" => {
615            omena_abstract_value::ClassValueFlowTransferV0::ConcatFacts(abstract_value_facts(facts))
616        }
617        _ => omena_abstract_value::ClassValueFlowTransferV0::Join,
618    }
619}
620
621fn type_fact_class_boundary_effect(
622    block: &TypeFactControlFlowBlockV2,
623) -> omena_abstract_value::ClassBoundaryEffectV0 {
624    match block.boundary_effect.as_str() {
625        "concatInsideToken" => omena_abstract_value::ClassBoundaryEffectV0::ConcatInsideToken,
626        "concatAtTokenBoundary" => {
627            omena_abstract_value::ClassBoundaryEffectV0::ConcatAtTokenBoundary
628        }
629        _ => omena_abstract_value::ClassBoundaryEffectV0::UnknownBoundary,
630    }
631}
632
633#[cfg(test)]
634mod tests {
635    use super::{
636        collect_expression_domain_flow_graphs,
637        collect_expression_domain_selector_certainty_flow_hedges,
638        summarize_expression_domain_call_site_flow_analysis_input,
639        summarize_expression_domain_candidates_input,
640        summarize_expression_domain_canonical_candidate_bundle_input,
641        summarize_expression_domain_canonical_producer_signal_input,
642        summarize_expression_domain_control_flow_analysis_input,
643        summarize_expression_domain_evaluator_candidates_input,
644        summarize_expression_domain_flow_analysis_input,
645        summarize_expression_domain_fragments_input, summarize_expression_domain_plan_input,
646        summarize_expression_domain_provenance_explanations_input,
647        summarize_expression_domain_reduced_product_iteration_input,
648    };
649    use crate::{
650        StringTypeFactsV2, TypeFactControlFlowBlockV2, TypeFactControlFlowGraphV2, TypeFactEntryV2,
651        configure_nonconvergent_selector_certainty_fixture, test_support::sample_input,
652    };
653    use omena_abstract_value::AbstractClassValueV0;
654
655    #[test]
656    fn summarizes_expression_domain_counts() {
657        let summary = summarize_expression_domain_plan_input(&sample_input());
658
659        assert_eq!(
660            summary.planned_expression_ids,
661            vec!["expr-1".to_string(), "expr-2".to_string()]
662        );
663        assert_eq!(summary.value_domain_kinds.get("constrained"), Some(&1));
664        assert_eq!(summary.value_domain_kinds.get("finiteSet"), Some(&1));
665        assert_eq!(summary.value_constraint_kinds.get("prefixSuffix"), Some(&1));
666        assert_eq!(summary.constraint_detail_counts.prefix_count, 1);
667        assert_eq!(summary.constraint_detail_counts.suffix_count, 1);
668        assert_eq!(summary.constraint_detail_counts.min_len_count, 1);
669        assert_eq!(summary.finite_value_count, 2);
670    }
671
672    #[test]
673    fn summarizes_expression_domain_fragments() {
674        let summary = summarize_expression_domain_fragments_input(&sample_input());
675
676        assert_eq!(summary.fragments.len(), 2);
677        let first = &summary.fragments[0];
678        assert_eq!(first.expression_id, "expr-1");
679        assert_eq!(first.file_path, "/tmp/App.tsx");
680        assert_eq!(first.value_domain_kind, "constrained");
681        assert_eq!(first.value_constraint_kind.as_deref(), Some("prefixSuffix"));
682        assert_eq!(first.value_prefix.as_deref(), Some("btn-"));
683        assert_eq!(first.value_suffix.as_deref(), Some("-active"));
684        assert_eq!(first.value_min_len, Some(10));
685        assert_eq!(first.finite_value_count, 0);
686
687        let second = &summary.fragments[1];
688        assert_eq!(second.expression_id, "expr-2");
689        assert_eq!(second.value_domain_kind, "finiteSet");
690        assert_eq!(second.finite_value_count, 2);
691    }
692
693    #[test]
694    fn summarizes_expression_domain_candidates() {
695        let summary = summarize_expression_domain_candidates_input(&sample_input());
696
697        assert_eq!(summary.candidates.len(), 2);
698        assert_eq!(summary.candidates[0].expression_id, "expr-1");
699        assert_eq!(summary.candidates[0].value_domain_kind, "constrained");
700        assert_eq!(
701            summary.candidates[0].value_constraint_kind.as_deref(),
702            Some("prefixSuffix")
703        );
704        assert_eq!(summary.candidates[1].expression_id, "expr-2");
705        assert_eq!(summary.candidates[1].finite_value_count, 2);
706    }
707
708    #[test]
709    fn summarizes_expression_domain_canonical_candidate_bundle() {
710        let summary = summarize_expression_domain_canonical_candidate_bundle_input(&sample_input());
711
712        assert_eq!(summary.plan_summary.planned_expression_ids.len(), 2);
713        assert_eq!(summary.fragments.len(), 2);
714        assert_eq!(summary.candidates.len(), 2);
715    }
716
717    #[test]
718    fn summarizes_expression_domain_evaluator_candidates() {
719        let summary = summarize_expression_domain_evaluator_candidates_input(&sample_input());
720
721        assert_eq!(summary.schema_version, "0");
722        assert_eq!(summary.input_version, "2");
723        assert_eq!(summary.results.len(), 2);
724        assert_eq!(summary.results[0].kind, "expression-domain");
725        assert_eq!(summary.results[0].query_id, "expr-1");
726        assert_eq!(summary.results[0].payload.value_domain_kind, "prefixSuffix");
727        assert_eq!(
728            summary.results[0].payload.value_constraint_kind.as_deref(),
729            Some("prefixSuffix")
730        );
731        assert_eq!(summary.results[1].payload.finite_value_count, 2);
732    }
733
734    #[test]
735    fn summarizes_expression_domain_provenance_explanations() {
736        let summary = summarize_expression_domain_provenance_explanations_input(&sample_input());
737
738        assert_eq!(summary.schema_version, "0");
739        assert_eq!(
740            summary.product,
741            "engine-input-producers.expression-domain-provenance-explanations"
742        );
743        assert_eq!(summary.input_version, "2");
744        assert_eq!(summary.explanation_count, 2);
745        assert_eq!(summary.explanations[0].expression_id, "expr-1");
746        assert_eq!(summary.explanations[0].input_fact_kind, "constrained");
747        assert_eq!(
748            summary.explanations[0].input_constraint_kind.as_deref(),
749            Some("prefixSuffix")
750        );
751        assert_eq!(summary.explanations[0].reduced_kind, "prefixSuffix");
752        assert_eq!(
753            summary.explanations[0].derivation.product,
754            "omena-abstract-value.reduced-class-value-derivation"
755        );
756        assert_eq!(
757            summary.explanations[0].provenance_tree.product,
758            "omena-abstract-value.provenance-tree"
759        );
760        assert_eq!(
761            summary.explanations[0].provenance_tree.root.operation,
762            "constraintDomain"
763        );
764    }
765
766    #[test]
767    fn expression_domain_evaluator_reports_reduced_value_domain_kind() {
768        let mut input = sample_input();
769        input.type_facts.push(TypeFactEntryV2 {
770            file_path: "/tmp/App.tsx".to_string(),
771            expression_id: "expr-3".to_string(),
772            facts: StringTypeFactsV2 {
773                kind: "finiteSet".to_string(),
774                constraint_kind: Some("prefix".to_string()),
775                values: Some(vec!["btn-active".to_string(), "card".to_string()]),
776                prefix: Some("btn-".to_string()),
777                suffix: None,
778                min_len: None,
779                max_len: None,
780                char_must: None,
781                char_may: None,
782                may_include_other_chars: None,
783                provenance: None,
784            },
785            control_flow_graph: None,
786        });
787
788        let fragments = summarize_expression_domain_fragments_input(&input);
789        let candidates = summarize_expression_domain_candidates_input(&input);
790        let evaluator_candidates = summarize_expression_domain_evaluator_candidates_input(&input);
791
792        assert_eq!(fragments.fragments[2].expression_id, "expr-3");
793        assert_eq!(fragments.fragments[2].value_domain_kind, "finiteSet");
794        assert_eq!(candidates.candidates[2].expression_id, "expr-3");
795        assert_eq!(candidates.candidates[2].value_domain_kind, "finiteSet");
796        assert_eq!(evaluator_candidates.results[2].query_id, "expr-3");
797        assert_eq!(
798            evaluator_candidates.results[2].payload.value_domain_kind,
799            "exact"
800        );
801        assert_eq!(
802            evaluator_candidates.results[2]
803                .payload
804                .value_domain_derivation
805                .reduced_kind,
806            "exact"
807        );
808        assert_eq!(
809            evaluator_candidates.results[2]
810                .payload
811                .value_domain_derivation
812                .steps[1]
813                .operation,
814            "intersectConstraint"
815        );
816        assert_eq!(
817            evaluator_candidates.results[2]
818                .payload
819                .value_domain_provenance_tree
820                .product,
821            "omena-abstract-value.provenance-tree"
822        );
823        assert_eq!(
824            evaluator_candidates.results[2]
825                .payload
826                .value_domain_provenance_tree
827                .root
828                .operation,
829            "exactLiteral"
830        );
831    }
832
833    #[test]
834    fn summarizes_expression_domain_flow_analysis() -> Result<(), serde_json::Error> {
835        let mut input = sample_input();
836        input.type_facts = vec![
837            exact_type_fact("expr-branch-a", "btn-primary"),
838            exact_type_fact("expr-branch-b", "btn-secondary"),
839            exact_type_fact("expr-branch-c", "card"),
840        ];
841
842        let summary = summarize_expression_domain_flow_analysis_input(&input);
843
844        assert_eq!(summary.schema_version, "0");
845        assert_eq!(
846            summary.product,
847            "engine-input-producers.expression-domain-flow-analysis"
848        );
849        assert_eq!(summary.analyses.len(), 3);
850        assert_eq!(summary.analyses[0].file_path, "/tmp/App.tsx");
851        assert_eq!(
852            summary.analyses[0].analysis.context_sensitivity,
853            "perSuppliedGraph"
854        );
855        let wire = serde_json::to_value(&summary)?;
856        assert_eq!(
857            wire["analyses"][0]["analysis"]["contextSensitivity"],
858            "perSuppliedGraph"
859        );
860        assert!(
861            summary
862                .analyses
863                .iter()
864                .all(|entry| entry.analysis.converged)
865        );
866        assert_eq!(
867            summary.analyses[0]
868                .analysis
869                .nodes
870                .iter()
871                .find(|node| node.id == "expr-branch-a")
872                .map(|node| (node.value_kind, &node.value)),
873            Some((
874                "exact",
875                &AbstractClassValueV0::Exact {
876                    value: "btn-primary".to_string()
877                }
878            ))
879        );
880        assert!(summary.analyses.iter().all(|entry| {
881            entry
882                .analysis
883                .nodes
884                .iter()
885                .all(|node| node.id != "file-merge")
886        }));
887        Ok(())
888    }
889
890    #[test]
891    fn exposes_expression_domain_flow_graphs_for_query_runtime_reuse() {
892        let mut input = sample_input();
893        input.type_facts = vec![
894            exact_type_fact("expr-branch-a", "btn-primary"),
895            exact_type_fact("expr-branch-b", "btn-secondary"),
896        ];
897
898        let graphs = collect_expression_domain_flow_graphs(&input);
899
900        assert_eq!(graphs.len(), 2);
901        assert_eq!(
902            graphs
903                .iter()
904                .map(|entry| entry.graph_id.as_str())
905                .collect::<Vec<_>>(),
906            vec![
907                "/tmp/App.tsx:expr-branch-a:expression-domain-flow",
908                "/tmp/App.tsx:expr-branch-b:expression-domain-flow"
909            ]
910        );
911        assert!(graphs.iter().all(|entry| {
912            entry.graph.context_key.as_deref() == Some(entry.graph_id.as_str())
913                && entry.graph.nodes.iter().all(|node| node.id != "file-merge")
914        }));
915    }
916
917    #[test]
918    fn does_not_synthesize_control_flow_analysis_without_source_cfg() {
919        let mut input = sample_input();
920        input.type_facts = vec![
921            exact_type_fact("expr-branch-a", "btn-primary"),
922            exact_type_fact("expr-branch-b", "btn-secondary"),
923        ];
924
925        let summary = summarize_expression_domain_control_flow_analysis_input(&input);
926
927        assert_eq!(
928            summary.product,
929            "engine-input-producers.expression-domain-control-flow-analysis"
930        );
931        assert!(summary.analyses.is_empty());
932    }
933
934    #[test]
935    fn selector_certainty_flow_hedge_collector_records_nonconvergent_source_cfg() {
936        let mut input = sample_input();
937        configure_nonconvergent_selector_certainty_fixture(&mut input.type_facts[0], "x");
938
939        let hedges = collect_expression_domain_selector_certainty_flow_hedges(&input);
940        let key = ("/tmp/App.tsx".to_string(), "expr-1".to_string());
941
942        assert!(hedges.contains_key(&key));
943        let hedge = &hedges[&key];
944        assert!(!hedge.graph_converged);
945        assert!(hedge.contains_flow_iteration_limit);
946    }
947
948    #[test]
949    #[should_panic(expected = "selector-certainty flow hedge duplicate type-fact key")]
950    fn duplicate_selector_certainty_type_fact_key_fails_closed() {
951        let mut input = sample_input();
952        configure_nonconvergent_selector_certainty_fixture(&mut input.type_facts[0], "x");
953        input.type_facts.push(input.type_facts[0].clone());
954
955        let _ = collect_expression_domain_selector_certainty_flow_hedges(&input);
956    }
957
958    #[test]
959    fn consumes_type_fact_control_flow_graph_for_branchy_flow() {
960        let mut input = sample_input();
961        input.type_facts = vec![TypeFactEntryV2 {
962            file_path: "/tmp/App.tsx".to_string(),
963            expression_id: "expr-branchy".to_string(),
964            facts: StringTypeFactsV2 {
965                kind: "exact".to_string(),
966                constraint_kind: None,
967                values: Some(vec!["btn-primary".to_string()]),
968                prefix: None,
969                suffix: None,
970                min_len: None,
971                max_len: None,
972                char_must: None,
973                char_may: None,
974                may_include_other_chars: None,
975                provenance: None,
976            },
977            control_flow_graph: Some(branchy_type_fact_control_flow_graph()),
978        }];
979
980        let summary = summarize_expression_domain_control_flow_analysis_input(&input);
981
982        assert_eq!(summary.analyses.len(), 1);
983        assert_eq!(
984            summary.analyses[0].graph_id,
985            "/tmp/App.tsx:expr-branchy:expression-domain-control-flow"
986        );
987        let analysis = &summary.analyses[0].analysis;
988        assert_eq!(analysis.block_count, 6);
989        assert_eq!(analysis.edge_count, 6);
990        assert_eq!(analysis.branch_block_ids, vec!["branch:0".to_string()]);
991        assert_eq!(analysis.join_block_ids, vec!["join:0".to_string()]);
992        assert!(
993            analysis
994                .blocks
995                .iter()
996                .all(|block| block.block_id != "file-merge")
997        );
998        assert!(
999            analysis
1000                .blocks
1001                .iter()
1002                .find(|block| block.block_id == "branch:0")
1003                .is_some_and(|block| block.successor_block_ids.len() > 1)
1004        );
1005        assert!(analysis
1006            .flow_analysis
1007            .nodes
1008            .iter()
1009            .any(|node| node.id == "expr-branchy:then:0"
1010                && node.transfer_kind == "concatFacts"));
1011    }
1012
1013    #[test]
1014    fn control_flow_blocks_prefer_source_frontend_facts_when_present() {
1015        let mut graph = branchy_type_fact_control_flow_graph();
1016        if let Some(block) = graph.blocks.iter_mut().find(|block| block.id == "then:0") {
1017            block.facts = Some(exact_type_fact("expr-block", "btn-secondary").facts);
1018        }
1019        let mut input = sample_input();
1020        input.type_facts = vec![TypeFactEntryV2 {
1021            file_path: "/tmp/App.tsx".to_string(),
1022            expression_id: "expr-branchy".to_string(),
1023            facts: StringTypeFactsV2 {
1024                kind: "unknown".to_string(),
1025                constraint_kind: None,
1026                values: None,
1027                prefix: None,
1028                suffix: None,
1029                min_len: None,
1030                max_len: None,
1031                char_must: None,
1032                char_may: None,
1033                may_include_other_chars: None,
1034                provenance: None,
1035            },
1036            control_flow_graph: Some(graph),
1037        }];
1038
1039        let summary = summarize_expression_domain_control_flow_analysis_input(&input);
1040        let matching_nodes = summary.analyses[0]
1041            .analysis
1042            .flow_analysis
1043            .nodes
1044            .iter()
1045            .filter(|node| node.id == "expr-branchy:then:0")
1046            .collect::<Vec<_>>();
1047        assert_eq!(matching_nodes.len(), 1);
1048        let node = matching_nodes[0];
1049
1050        assert_eq!(node.transfer_kind, "assignFacts");
1051        assert_eq!(
1052            node.value,
1053            AbstractClassValueV0::Exact {
1054                value: "btn-secondary".to_string()
1055            }
1056        );
1057    }
1058
1059    #[test]
1060    fn summarizes_expression_domain_call_site_flow_analysis_for_zero_and_one_cfa() {
1061        let mut input = sample_input();
1062        input.type_facts = vec![
1063            exact_type_fact_in_file("/tmp/App.tsx", "expr-primary", "btn-primary"),
1064            exact_type_fact_in_file("/tmp/Card.tsx", "expr-secondary", "btn-secondary"),
1065        ];
1066
1067        let summary = summarize_expression_domain_call_site_flow_analysis_input(&input);
1068
1069        assert_eq!(summary.schema_version, "0");
1070        assert_eq!(
1071            summary.product,
1072            "engine-input-producers.expression-domain-call-site-flow-analysis"
1073        );
1074        assert_eq!(summary.zero_cfa.context_sensitivity, "0-cfa");
1075        assert_eq!(summary.one_cfa.context_sensitivity, "1-cfa");
1076        assert_eq!(summary.zero_cfa.call_site_count, 2);
1077        assert_eq!(summary.one_cfa.call_site_count, 2);
1078        assert_eq!(
1079            summary.zero_cfa.entries[0].context_key,
1080            "expression-domain-class-value@<root>"
1081        );
1082        assert_eq!(
1083            summary.zero_cfa.entries[1].context_key,
1084            "expression-domain-class-value@<root>"
1085        );
1086        assert_ne!(
1087            summary.one_cfa.entries[0].context_key,
1088            summary.one_cfa.entries[1].context_key
1089        );
1090        assert_eq!(
1091            summary.zero_cfa.entries[0].exit_value,
1092            AbstractClassValueV0::FiniteSet {
1093                values: vec!["btn-primary".to_string(), "btn-secondary".to_string()]
1094            }
1095        );
1096        assert_eq!(
1097            summary.zero_cfa.entries[1].exit_value,
1098            summary.zero_cfa.entries[0].exit_value
1099        );
1100        assert_eq!(
1101            summary.one_cfa.entries[0].exit_value,
1102            AbstractClassValueV0::Exact {
1103                value: "btn-primary".to_string()
1104            }
1105        );
1106        assert_eq!(
1107            summary.one_cfa.entries[1].exit_value,
1108            AbstractClassValueV0::Exact {
1109                value: "btn-secondary".to_string()
1110            }
1111        );
1112    }
1113
1114    #[test]
1115    fn summarizes_expression_domain_reduced_product_iteration() {
1116        let mut input = sample_input();
1117        input.type_facts = vec![TypeFactEntryV2 {
1118            file_path: "/tmp/App.tsx".to_string(),
1119            expression_id: "expr-reduced".to_string(),
1120            facts: StringTypeFactsV2 {
1121                kind: "constrained".to_string(),
1122                constraint_kind: Some("composite".to_string()),
1123                values: None,
1124                prefix: Some("btn-".to_string()),
1125                suffix: Some("-active".to_string()),
1126                min_len: None,
1127                max_len: None,
1128                char_must: Some("a".to_string()),
1129                char_may: Some("-abceintv".to_string()),
1130                may_include_other_chars: Some(false),
1131                provenance: None,
1132            },
1133            control_flow_graph: None,
1134        }];
1135
1136        let summary = summarize_expression_domain_reduced_product_iteration_input(&input);
1137
1138        assert_eq!(summary.schema_version, "0");
1139        assert_eq!(
1140            summary.product,
1141            "engine-input-producers.expression-domain-reduced-product-iteration"
1142        );
1143        assert_eq!(summary.input_version, "2");
1144        assert_eq!(summary.iteration_count, 1);
1145        assert_eq!(summary.iterations[0].expression_id, "expr-reduced");
1146        assert_eq!(summary.iterations[0].axis_constraint_count, 3);
1147        assert_eq!(summary.iterations[0].input_value_kind, "composite");
1148        assert_eq!(summary.iterations[0].iteration.input_count, 3);
1149        assert_eq!(summary.iterations[0].iteration.applied_constraint_count, 3);
1150        assert!(summary.iterations[0].iteration.converged);
1151        assert!(summary.iterations[0].iteration.monotone_witness_valid);
1152        assert_eq!(summary.iterations[0].iteration.result_kind, "composite");
1153    }
1154
1155    #[test]
1156    fn summarizes_expression_domain_canonical_producer_signal() {
1157        let summary = summarize_expression_domain_canonical_producer_signal_input(&sample_input());
1158
1159        assert_eq!(summary.schema_version, "0");
1160        assert_eq!(summary.input_version, "2");
1161        assert_eq!(
1162            summary
1163                .canonical_bundle
1164                .plan_summary
1165                .planned_expression_ids
1166                .len(),
1167            2
1168        );
1169        assert_eq!(summary.canonical_bundle.fragments.len(), 2);
1170        assert_eq!(summary.canonical_bundle.candidates.len(), 2);
1171        assert_eq!(summary.evaluator_candidates.results.len(), 2);
1172    }
1173
1174    fn exact_type_fact(expression_id: &str, value: &str) -> TypeFactEntryV2 {
1175        exact_type_fact_in_file("/tmp/App.tsx", expression_id, value)
1176    }
1177
1178    fn exact_type_fact_in_file(
1179        file_path: &str,
1180        expression_id: &str,
1181        value: &str,
1182    ) -> TypeFactEntryV2 {
1183        TypeFactEntryV2 {
1184            file_path: file_path.to_string(),
1185            expression_id: expression_id.to_string(),
1186            facts: StringTypeFactsV2 {
1187                kind: "exact".to_string(),
1188                constraint_kind: None,
1189                values: Some(vec![value.to_string()]),
1190                prefix: None,
1191                suffix: None,
1192                min_len: None,
1193                max_len: None,
1194                char_must: None,
1195                char_may: None,
1196                may_include_other_chars: None,
1197                provenance: None,
1198            },
1199            control_flow_graph: None,
1200        }
1201    }
1202
1203    fn branchy_type_fact_control_flow_graph() -> TypeFactControlFlowGraphV2 {
1204        TypeFactControlFlowGraphV2 {
1205            entry_block_id: "entry".to_string(),
1206            blocks: vec![
1207                type_fact_control_flow_block("entry", "entry", "entry", &["branch:0"]),
1208                type_fact_control_flow_block("branch:0", "branch", "branch", &["then:0", "else:0"]),
1209                type_fact_control_flow_block("then:0", "assignment", "concatFacts", &["join:0"]),
1210                type_fact_control_flow_block("else:0", "assignment", "assignFacts", &["join:0"]),
1211                type_fact_control_flow_block("join:0", "join", "join", &["exit"]),
1212                type_fact_control_flow_block("exit", "exit", "exit", &[]),
1213            ],
1214        }
1215    }
1216
1217    fn type_fact_control_flow_block(
1218        id: &str,
1219        kind: &str,
1220        transfer_kind: &str,
1221        successor_block_ids: &[&str],
1222    ) -> TypeFactControlFlowBlockV2 {
1223        TypeFactControlFlowBlockV2 {
1224            id: id.to_string(),
1225            kind: kind.to_string(),
1226            transfer_kind: transfer_kind.to_string(),
1227            successor_block_ids: successor_block_ids
1228                .iter()
1229                .map(|id| (*id).to_string())
1230                .collect(),
1231            symbol_ordinal: None,
1232            variable_name: None,
1233            expression_kind: None,
1234            boundary_effect: "unknownBoundary".to_string(),
1235            facts: None,
1236        }
1237    }
1238}