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}