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