1use std::collections::BTreeMap;
2
3use crate::{
4 ConstraintDetailCounts, ConstraintDetailInput, EngineInputV2, ExpressionDomainCandidateV0,
5 ExpressionDomainCandidatesV0, ExpressionDomainCanonicalCandidateBundleV0,
6 ExpressionDomainCanonicalProducerSignalV0, ExpressionDomainControlFlowAnalysisEntryV0,
7 ExpressionDomainControlFlowAnalysisV0, ExpressionDomainEvaluatorCandidatePayloadV0,
8 ExpressionDomainEvaluatorCandidateV0, ExpressionDomainEvaluatorCandidatesV0,
9 ExpressionDomainFlowAnalysisEntryV0, ExpressionDomainFlowAnalysisV0,
10 ExpressionDomainFlowGraphEntryV0, ExpressionDomainFragmentV0, ExpressionDomainFragmentsV0,
11 ExpressionDomainPlanSummaryV0, TypeFactEntryV2, abstract_value_facts,
12 collect_constraint_detail_counts, map_reduced_expression_value_domain_derivation,
13 map_reduced_expression_value_domain_kind,
14};
15
16struct ExpressionDomainInputRows {
17 plan_summary: ExpressionDomainPlanSummaryV0,
18 fragments: Vec<ExpressionDomainFragmentV0>,
19 candidates: Vec<ExpressionDomainCandidateV0>,
20 evaluator_candidates: Vec<ExpressionDomainEvaluatorCandidateV0>,
21}
22
23fn collect_expression_domain_input_rows(input: &EngineInputV2) -> ExpressionDomainInputRows {
24 let mut planned_expression_ids = Vec::new();
25 let mut value_domain_kinds = BTreeMap::new();
26 let mut value_constraint_kinds = BTreeMap::new();
27 let mut constraint_detail_counts = ConstraintDetailCounts::default();
28 let mut finite_value_count = 0usize;
29 let mut fragments = Vec::new();
30 let mut candidates = Vec::new();
31 let mut evaluator_candidates = Vec::new();
32
33 for entry in &input.type_facts {
34 planned_expression_ids.push(entry.expression_id.clone());
35 *value_domain_kinds
36 .entry(entry.facts.kind.clone())
37 .or_insert(0) += 1;
38
39 if let Some(values) = &entry.facts.values {
40 finite_value_count += values.len();
41 }
42
43 if let Some(constraint_kind) = &entry.facts.constraint_kind {
44 *value_constraint_kinds
45 .entry(constraint_kind.clone())
46 .or_insert(0) += 1;
47 }
48
49 collect_constraint_detail_counts(
50 &mut constraint_detail_counts,
51 ConstraintDetailInput {
52 prefix: entry.facts.prefix.as_ref(),
53 suffix: entry.facts.suffix.as_ref(),
54 min_len: entry.facts.min_len,
55 max_len: entry.facts.max_len,
56 char_must: entry.facts.char_must.as_ref(),
57 char_may: entry.facts.char_may.as_ref(),
58 may_include_other_chars: entry.facts.may_include_other_chars,
59 },
60 );
61
62 let fragment = ExpressionDomainFragmentV0 {
63 expression_id: entry.expression_id.clone(),
64 file_path: entry.file_path.clone(),
65 value_domain_kind: entry.facts.kind.clone(),
66 value_constraint_kind: entry.facts.constraint_kind.clone(),
67 value_prefix: entry.facts.prefix.clone(),
68 value_suffix: entry.facts.suffix.clone(),
69 value_min_len: entry.facts.min_len,
70 value_max_len: entry.facts.max_len,
71 value_char_must: entry.facts.char_must.clone(),
72 value_char_may: entry.facts.char_may.clone(),
73 value_may_include_other_chars: entry.facts.may_include_other_chars,
74 finite_value_count: entry.facts.values.as_ref().map_or(0, Vec::len),
75 };
76 fragments.push(fragment.clone());
77 candidates.push(ExpressionDomainCandidateV0 {
78 expression_id: fragment.expression_id,
79 file_path: fragment.file_path,
80 value_domain_kind: fragment.value_domain_kind,
81 value_constraint_kind: fragment.value_constraint_kind,
82 value_prefix: fragment.value_prefix,
83 value_suffix: fragment.value_suffix,
84 value_min_len: fragment.value_min_len,
85 value_max_len: fragment.value_max_len,
86 value_char_must: fragment.value_char_must,
87 value_char_may: fragment.value_char_may,
88 value_may_include_other_chars: fragment.value_may_include_other_chars,
89 finite_value_count: fragment.finite_value_count,
90 });
91
92 evaluator_candidates.push(ExpressionDomainEvaluatorCandidateV0 {
93 kind: "expression-domain",
94 file_path: entry.file_path.clone(),
95 query_id: entry.expression_id.clone(),
96 payload: ExpressionDomainEvaluatorCandidatePayloadV0 {
97 expression_id: entry.expression_id.clone(),
98 value_domain_kind: map_reduced_expression_value_domain_kind(&entry.facts),
99 value_constraint_kind: entry.facts.constraint_kind.clone(),
100 value_prefix: entry.facts.prefix.clone(),
101 value_suffix: entry.facts.suffix.clone(),
102 value_min_len: entry.facts.min_len,
103 value_max_len: entry.facts.max_len,
104 value_char_must: entry.facts.char_must.clone(),
105 value_char_may: entry.facts.char_may.clone(),
106 value_may_include_other_chars: entry.facts.may_include_other_chars,
107 finite_value_count: entry.facts.values.as_ref().map_or(0, Vec::len),
108 value_domain_derivation: map_reduced_expression_value_domain_derivation(
109 &entry.facts,
110 ),
111 },
112 });
113 }
114
115 fragments.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
116 candidates.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
117 evaluator_candidates.sort_by(|a, b| a.query_id.cmp(&b.query_id));
118
119 ExpressionDomainInputRows {
120 plan_summary: ExpressionDomainPlanSummaryV0 {
121 schema_version: "0",
122 input_version: input.version.clone(),
123 planned_expression_ids,
124 value_domain_kinds,
125 value_constraint_kinds,
126 constraint_detail_counts,
127 finite_value_count,
128 },
129 fragments,
130 candidates,
131 evaluator_candidates,
132 }
133}
134
135pub fn summarize_expression_domain_plan_input(
136 input: &EngineInputV2,
137) -> ExpressionDomainPlanSummaryV0 {
138 collect_expression_domain_input_rows(input).plan_summary
139}
140
141pub fn summarize_expression_domain_fragments_input(
142 input: &EngineInputV2,
143) -> ExpressionDomainFragmentsV0 {
144 let rows = collect_expression_domain_input_rows(input);
145
146 ExpressionDomainFragmentsV0 {
147 schema_version: "0",
148 input_version: input.version.clone(),
149 fragments: rows.fragments,
150 }
151}
152
153pub fn summarize_expression_domain_candidates_input(
154 input: &EngineInputV2,
155) -> ExpressionDomainCandidatesV0 {
156 let rows = collect_expression_domain_input_rows(input);
157
158 ExpressionDomainCandidatesV0 {
159 schema_version: "0",
160 input_version: input.version.clone(),
161 candidates: rows.candidates,
162 }
163}
164
165pub fn summarize_expression_domain_canonical_candidate_bundle_input(
166 input: &EngineInputV2,
167) -> ExpressionDomainCanonicalCandidateBundleV0 {
168 let rows = collect_expression_domain_input_rows(input);
169
170 ExpressionDomainCanonicalCandidateBundleV0 {
171 schema_version: "0",
172 input_version: input.version.clone(),
173 plan_summary: rows.plan_summary,
174 fragments: rows.fragments,
175 candidates: rows.candidates,
176 }
177}
178
179pub fn summarize_expression_domain_evaluator_candidates_input(
180 input: &EngineInputV2,
181) -> ExpressionDomainEvaluatorCandidatesV0 {
182 let rows = collect_expression_domain_input_rows(input);
183
184 ExpressionDomainEvaluatorCandidatesV0 {
185 schema_version: "0",
186 input_version: input.version.clone(),
187 results: rows.evaluator_candidates,
188 }
189}
190
191pub fn summarize_expression_domain_canonical_producer_signal_input(
192 input: &EngineInputV2,
193) -> ExpressionDomainCanonicalProducerSignalV0 {
194 let rows = collect_expression_domain_input_rows(input);
195 let input_version = input.version.clone();
196
197 ExpressionDomainCanonicalProducerSignalV0 {
198 schema_version: "0",
199 input_version: input_version.clone(),
200 canonical_bundle: ExpressionDomainCanonicalCandidateBundleV0 {
201 schema_version: "0",
202 input_version: input_version.clone(),
203 plan_summary: rows.plan_summary,
204 fragments: rows.fragments,
205 candidates: rows.candidates,
206 },
207 evaluator_candidates: ExpressionDomainEvaluatorCandidatesV0 {
208 schema_version: "0",
209 input_version,
210 results: rows.evaluator_candidates,
211 },
212 }
213}
214
215pub fn summarize_expression_domain_flow_analysis_input(
216 input: &EngineInputV2,
217) -> ExpressionDomainFlowAnalysisV0 {
218 let analyses = collect_expression_domain_flow_graphs(input)
219 .into_iter()
220 .map(|entry| ExpressionDomainFlowAnalysisEntryV0 {
221 graph_id: entry.graph_id,
222 file_path: entry.file_path,
223 analysis: omena_abstract_value::analyze_class_value_flow(&entry.graph),
224 })
225 .collect();
226
227 ExpressionDomainFlowAnalysisV0 {
228 schema_version: "0",
229 product: "engine-input-producers.expression-domain-flow-analysis",
230 input_version: input.version.clone(),
231 analyses,
232 }
233}
234
235pub fn summarize_expression_domain_control_flow_analysis_input(
236 input: &EngineInputV2,
237) -> ExpressionDomainControlFlowAnalysisV0 {
238 let analyses = collect_expression_domain_flow_graphs(input)
239 .into_iter()
240 .map(|entry| {
241 let cfg = expression_domain_control_flow_graph(&entry.graph);
242 ExpressionDomainControlFlowAnalysisEntryV0 {
243 graph_id: entry.graph_id,
244 file_path: entry.file_path,
245 analysis: omena_abstract_value::analyze_class_value_control_flow_graph(&cfg),
246 }
247 })
248 .collect();
249
250 ExpressionDomainControlFlowAnalysisV0 {
251 schema_version: "0",
252 product: "engine-input-producers.expression-domain-control-flow-analysis",
253 input_version: input.version.clone(),
254 analyses,
255 }
256}
257
258pub fn collect_expression_domain_flow_graphs(
259 input: &EngineInputV2,
260) -> Vec<ExpressionDomainFlowGraphEntryV0> {
261 let mut by_file = BTreeMap::<String, Vec<&TypeFactEntryV2>>::new();
262 for entry in &input.type_facts {
263 by_file
264 .entry(entry.file_path.clone())
265 .or_default()
266 .push(entry);
267 }
268
269 by_file
270 .into_iter()
271 .map(|(file_path, mut entries)| {
272 entries.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
273 let graph_id = format!("{file_path}:expression-domain-flow");
274 let mut nodes = entries
275 .iter()
276 .map(|entry| omena_abstract_value::ClassValueFlowNodeV0 {
277 id: entry.expression_id.clone(),
278 predecessors: Vec::new(),
279 transfer: omena_abstract_value::ClassValueFlowTransferV0::AssignFacts(
280 abstract_value_facts(&entry.facts),
281 ),
282 })
283 .collect::<Vec<_>>();
284
285 if entries.len() > 1 {
286 nodes.push(omena_abstract_value::ClassValueFlowNodeV0 {
287 id: "file-merge".to_string(),
288 predecessors: entries
289 .iter()
290 .map(|entry| entry.expression_id.clone())
291 .collect(),
292 transfer: omena_abstract_value::ClassValueFlowTransferV0::Join,
293 });
294 }
295
296 let graph = omena_abstract_value::ClassValueFlowGraphV0 {
297 context_key: Some(graph_id.clone()),
298 nodes,
299 };
300
301 ExpressionDomainFlowGraphEntryV0 {
302 graph_id,
303 file_path,
304 graph,
305 }
306 })
307 .collect()
308}
309
310fn expression_domain_control_flow_graph(
311 graph: &omena_abstract_value::ClassValueFlowGraphV0,
312) -> omena_abstract_value::ClassValueControlFlowGraphV0 {
313 let merge_node_id = "file-merge";
314 let has_merge = graph.nodes.iter().any(|node| node.id == merge_node_id);
315 let mut blocks = Vec::new();
316
317 if has_merge {
318 blocks.push(omena_abstract_value::ClassValueControlFlowBlockV0 {
319 id: "entry".to_string(),
320 nodes: Vec::new(),
321 successor_block_ids: graph
322 .nodes
323 .iter()
324 .filter(|node| node.id != merge_node_id)
325 .map(|node| format!("expr:{}", node.id))
326 .collect(),
327 });
328
329 for node in graph.nodes.iter().filter(|node| node.id != merge_node_id) {
330 blocks.push(omena_abstract_value::ClassValueControlFlowBlockV0 {
331 id: format!("expr:{}", node.id),
332 nodes: vec![node.clone()],
333 successor_block_ids: vec!["merge".to_string()],
334 });
335 }
336
337 if let Some(merge) = graph.nodes.iter().find(|node| node.id == merge_node_id) {
338 blocks.push(omena_abstract_value::ClassValueControlFlowBlockV0 {
339 id: "merge".to_string(),
340 nodes: vec![merge.clone()],
341 successor_block_ids: Vec::new(),
342 });
343 }
344 } else {
345 blocks.extend(graph.nodes.iter().map(|node| {
346 omena_abstract_value::ClassValueControlFlowBlockV0 {
347 id: format!("expr:{}", node.id),
348 nodes: vec![node.clone()],
349 successor_block_ids: Vec::new(),
350 }
351 }));
352 }
353
354 let entry_block_id = blocks
355 .first()
356 .map(|block| block.id.clone())
357 .unwrap_or_else(|| "entry".to_string());
358
359 omena_abstract_value::ClassValueControlFlowGraphV0 {
360 context_key: graph.context_key.clone(),
361 entry_block_id,
362 blocks,
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::{
369 collect_expression_domain_flow_graphs, summarize_expression_domain_candidates_input,
370 summarize_expression_domain_canonical_candidate_bundle_input,
371 summarize_expression_domain_canonical_producer_signal_input,
372 summarize_expression_domain_control_flow_analysis_input,
373 summarize_expression_domain_evaluator_candidates_input,
374 summarize_expression_domain_flow_analysis_input,
375 summarize_expression_domain_fragments_input, summarize_expression_domain_plan_input,
376 };
377 use crate::{StringTypeFactsV2, TypeFactEntryV2, test_support::sample_input};
378 use omena_abstract_value::AbstractClassValueV0;
379
380 #[test]
381 fn summarizes_expression_domain_counts() {
382 let summary = summarize_expression_domain_plan_input(&sample_input());
383
384 assert_eq!(
385 summary.planned_expression_ids,
386 vec!["expr-1".to_string(), "expr-2".to_string()]
387 );
388 assert_eq!(summary.value_domain_kinds.get("constrained"), Some(&1));
389 assert_eq!(summary.value_domain_kinds.get("finiteSet"), Some(&1));
390 assert_eq!(summary.value_constraint_kinds.get("prefixSuffix"), Some(&1));
391 assert_eq!(summary.constraint_detail_counts.prefix_count, 1);
392 assert_eq!(summary.constraint_detail_counts.suffix_count, 1);
393 assert_eq!(summary.constraint_detail_counts.min_len_count, 1);
394 assert_eq!(summary.finite_value_count, 2);
395 }
396
397 #[test]
398 fn summarizes_expression_domain_fragments() {
399 let summary = summarize_expression_domain_fragments_input(&sample_input());
400
401 assert_eq!(summary.fragments.len(), 2);
402 let first = &summary.fragments[0];
403 assert_eq!(first.expression_id, "expr-1");
404 assert_eq!(first.file_path, "/tmp/App.tsx");
405 assert_eq!(first.value_domain_kind, "constrained");
406 assert_eq!(first.value_constraint_kind.as_deref(), Some("prefixSuffix"));
407 assert_eq!(first.value_prefix.as_deref(), Some("btn-"));
408 assert_eq!(first.value_suffix.as_deref(), Some("-active"));
409 assert_eq!(first.value_min_len, Some(10));
410 assert_eq!(first.finite_value_count, 0);
411
412 let second = &summary.fragments[1];
413 assert_eq!(second.expression_id, "expr-2");
414 assert_eq!(second.value_domain_kind, "finiteSet");
415 assert_eq!(second.finite_value_count, 2);
416 }
417
418 #[test]
419 fn summarizes_expression_domain_candidates() {
420 let summary = summarize_expression_domain_candidates_input(&sample_input());
421
422 assert_eq!(summary.candidates.len(), 2);
423 assert_eq!(summary.candidates[0].expression_id, "expr-1");
424 assert_eq!(summary.candidates[0].value_domain_kind, "constrained");
425 assert_eq!(
426 summary.candidates[0].value_constraint_kind.as_deref(),
427 Some("prefixSuffix")
428 );
429 assert_eq!(summary.candidates[1].expression_id, "expr-2");
430 assert_eq!(summary.candidates[1].finite_value_count, 2);
431 }
432
433 #[test]
434 fn summarizes_expression_domain_canonical_candidate_bundle() {
435 let summary = summarize_expression_domain_canonical_candidate_bundle_input(&sample_input());
436
437 assert_eq!(summary.plan_summary.planned_expression_ids.len(), 2);
438 assert_eq!(summary.fragments.len(), 2);
439 assert_eq!(summary.candidates.len(), 2);
440 }
441
442 #[test]
443 fn summarizes_expression_domain_evaluator_candidates() {
444 let summary = summarize_expression_domain_evaluator_candidates_input(&sample_input());
445
446 assert_eq!(summary.schema_version, "0");
447 assert_eq!(summary.input_version, "2");
448 assert_eq!(summary.results.len(), 2);
449 assert_eq!(summary.results[0].kind, "expression-domain");
450 assert_eq!(summary.results[0].query_id, "expr-1");
451 assert_eq!(summary.results[0].payload.value_domain_kind, "prefixSuffix");
452 assert_eq!(
453 summary.results[0].payload.value_constraint_kind.as_deref(),
454 Some("prefixSuffix")
455 );
456 assert_eq!(summary.results[1].payload.finite_value_count, 2);
457 }
458
459 #[test]
460 fn expression_domain_evaluator_reports_reduced_value_domain_kind() {
461 let mut input = sample_input();
462 input.type_facts.push(TypeFactEntryV2 {
463 file_path: "/tmp/App.tsx".to_string(),
464 expression_id: "expr-3".to_string(),
465 facts: StringTypeFactsV2 {
466 kind: "finiteSet".to_string(),
467 constraint_kind: Some("prefix".to_string()),
468 values: Some(vec!["btn-active".to_string(), "card".to_string()]),
469 prefix: Some("btn-".to_string()),
470 suffix: None,
471 min_len: None,
472 max_len: None,
473 char_must: None,
474 char_may: None,
475 may_include_other_chars: None,
476 },
477 });
478
479 let fragments = summarize_expression_domain_fragments_input(&input);
480 let candidates = summarize_expression_domain_candidates_input(&input);
481 let evaluator_candidates = summarize_expression_domain_evaluator_candidates_input(&input);
482
483 assert_eq!(fragments.fragments[2].expression_id, "expr-3");
484 assert_eq!(fragments.fragments[2].value_domain_kind, "finiteSet");
485 assert_eq!(candidates.candidates[2].expression_id, "expr-3");
486 assert_eq!(candidates.candidates[2].value_domain_kind, "finiteSet");
487 assert_eq!(evaluator_candidates.results[2].query_id, "expr-3");
488 assert_eq!(
489 evaluator_candidates.results[2].payload.value_domain_kind,
490 "exact"
491 );
492 assert_eq!(
493 evaluator_candidates.results[2]
494 .payload
495 .value_domain_derivation
496 .reduced_kind,
497 "exact"
498 );
499 assert_eq!(
500 evaluator_candidates.results[2]
501 .payload
502 .value_domain_derivation
503 .steps[1]
504 .operation,
505 "intersectConstraint"
506 );
507 }
508
509 #[test]
510 fn summarizes_expression_domain_flow_analysis() {
511 let mut input = sample_input();
512 input.type_facts = vec![
513 exact_type_fact("expr-branch-a", "btn-primary"),
514 exact_type_fact("expr-branch-b", "btn-secondary"),
515 exact_type_fact("expr-branch-c", "card"),
516 ];
517
518 let summary = summarize_expression_domain_flow_analysis_input(&input);
519
520 assert_eq!(summary.schema_version, "0");
521 assert_eq!(
522 summary.product,
523 "engine-input-producers.expression-domain-flow-analysis"
524 );
525 assert_eq!(summary.analyses.len(), 1);
526 assert_eq!(summary.analyses[0].file_path, "/tmp/App.tsx");
527 assert_eq!(summary.analyses[0].analysis.context_sensitivity, "1-cfa");
528 assert!(summary.analyses[0].analysis.converged);
529 assert_eq!(
530 summary.analyses[0]
531 .analysis
532 .nodes
533 .iter()
534 .find(|node| node.id == "file-merge")
535 .map(|node| (node.value_kind, &node.value)),
536 Some((
537 "finiteSet",
538 &AbstractClassValueV0::FiniteSet {
539 values: vec![
540 "btn-primary".to_string(),
541 "btn-secondary".to_string(),
542 "card".to_string(),
543 ]
544 }
545 ))
546 );
547 }
548
549 #[test]
550 fn exposes_expression_domain_flow_graphs_for_query_runtime_reuse() {
551 let mut input = sample_input();
552 input.type_facts = vec![
553 exact_type_fact("expr-branch-a", "btn-primary"),
554 exact_type_fact("expr-branch-b", "btn-secondary"),
555 ];
556
557 let graphs = collect_expression_domain_flow_graphs(&input);
558
559 assert_eq!(graphs.len(), 1);
560 assert_eq!(graphs[0].graph_id, "/tmp/App.tsx:expression-domain-flow");
561 assert_eq!(
562 graphs[0].graph.context_key.as_deref(),
563 Some(graphs[0].graph_id.as_str())
564 );
565 assert!(
566 graphs[0]
567 .graph
568 .nodes
569 .iter()
570 .any(|node| node.id == "file-merge")
571 );
572 }
573
574 #[test]
575 fn summarizes_expression_domain_control_flow_analysis() {
576 let mut input = sample_input();
577 input.type_facts = vec![
578 exact_type_fact("expr-branch-a", "btn-primary"),
579 exact_type_fact("expr-branch-b", "btn-secondary"),
580 ];
581
582 let summary = summarize_expression_domain_control_flow_analysis_input(&input);
583
584 assert_eq!(
585 summary.product,
586 "engine-input-producers.expression-domain-control-flow-analysis"
587 );
588 assert_eq!(summary.analyses.len(), 1);
589 assert_eq!(summary.analyses[0].analysis.block_count, 4);
590 assert_eq!(summary.analyses[0].analysis.edge_count, 4);
591 assert_eq!(
592 summary.analyses[0].analysis.flow_analysis.product,
593 "omena-abstract-value.flow-analysis"
594 );
595 assert!(
596 summary.analyses[0]
597 .analysis
598 .unreachable_block_ids
599 .is_empty()
600 );
601 }
602
603 #[test]
604 fn summarizes_expression_domain_canonical_producer_signal() {
605 let summary = summarize_expression_domain_canonical_producer_signal_input(&sample_input());
606
607 assert_eq!(summary.schema_version, "0");
608 assert_eq!(summary.input_version, "2");
609 assert_eq!(
610 summary
611 .canonical_bundle
612 .plan_summary
613 .planned_expression_ids
614 .len(),
615 2
616 );
617 assert_eq!(summary.canonical_bundle.fragments.len(), 2);
618 assert_eq!(summary.canonical_bundle.candidates.len(), 2);
619 assert_eq!(summary.evaluator_candidates.results.len(), 2);
620 }
621
622 fn exact_type_fact(expression_id: &str, value: &str) -> TypeFactEntryV2 {
623 TypeFactEntryV2 {
624 file_path: "/tmp/App.tsx".to_string(),
625 expression_id: expression_id.to_string(),
626 facts: StringTypeFactsV2 {
627 kind: "exact".to_string(),
628 constraint_kind: None,
629 values: Some(vec![value.to_string()]),
630 prefix: None,
631 suffix: None,
632 min_len: None,
633 max_len: None,
634 char_must: None,
635 char_may: None,
636 may_include_other_chars: None,
637 },
638 }
639 }
640}