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}