1use std::{
8 collections::BTreeMap,
9 io::{self, IsTerminal, Write},
10 ops::ControlFlow,
11};
12
13use color_eyre::{eyre::eyre, Result};
14use colored::Colorize;
15use gammalooprs::integrands::process::ProcessIntegrand;
16use gammalooprs::model::Model;
17use gammalooprs::processes::{
18 GraphSelectionSignatureInventory, ProcessCollection, ProcessDefinition,
19 RaisedCutSignatureInventory,
20};
21use gammalooprs::settings::{global::GenerationSettings, RuntimeSettings};
22use tracing::{info, warn};
23
24use crate::{
25 commands::{
26 process_settings::{serialize_runtime_named_settings, ProcessSettingsCompletionEntry},
27 run::{prepare_command_histories_with_context, PreparedCommand, PreparedRun},
28 save::SaveState,
29 CommandExecution, Commands, StartCommandsBlock,
30 },
31 integrand_info::IntegrandKind,
32 render_smart_toml,
33 repl::{
34 IntegrandDetailCompletionEntry, IrProfileCompletionEntry, ModelVertexCompletionEntry,
35 ProcessCompletionEntry, ProcessKind,
36 },
37 state::{CommandHistory, CommandsBlock, ProcessRef, RunHistory, State},
38 CLISettings, ReadOnlyStateOrigin,
39};
40
41const MAX_DISPLAY_COMMAND_LINES: usize = 5;
42
43fn format_command_block_conflict_message(conflicting_blocks: &[String]) -> String {
44 match conflicting_blocks {
45 [] => String::new(),
46 [single] => format!(
47 "Run card command block '{}' redefines an existing block with different commands",
48 single
49 ),
50 many => format!(
51 "Run card command blocks {} redefine existing blocks with different commands",
52 many.iter()
53 .map(|name| format!("'{}'", name))
54 .collect::<Vec<_>>()
55 .join(", ")
56 ),
57 }
58}
59
60fn prompt_command_block_conflict_override(conflicting_blocks: &[String]) -> Result<bool> {
61 let prompt = format!(
62 "{}. Proceed anyway? (it may break reproducibility of the state from run.toml) [y/n] > ",
63 format_command_block_conflict_message(conflicting_blocks)
64 );
65 let mut stderr = io::stderr();
66 let mut input = String::new();
67 loop {
68 eprint!("{prompt}");
69 stderr.flush()?;
70 input.clear();
71 io::stdin().read_line(&mut input)?;
72 match input.trim().to_ascii_lowercase().as_str() {
73 "y" | "yes" => return Ok(true),
74 "n" | "no" => return Ok(false),
75 _ => {
76 eprintln!("Please answer 'y' or 'n'.");
77 }
78 }
79 }
80}
81
82#[derive(Debug, Clone, Default)]
87pub struct CliSessionState {
88 pending_commands_block: Option<PendingCommandsBlock>,
89}
90
91#[derive(Debug, Clone)]
92struct PendingCommandsBlock {
93 name: String,
94 commands: Vec<CommandHistory>,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98enum HistoryMode {
99 Record,
100 Suppress,
101}
102
103pub struct CliSession<'a> {
110 state: &'a mut State,
111 run_history: &'a mut RunHistory,
112 cli_settings: &'a mut CLISettings,
113 default_runtime_settings: &'a mut RuntimeSettings,
114 session_state: &'a mut CliSessionState,
115}
116
117impl<'a> CliSession<'a> {
118 pub fn new(
124 state: &'a mut State,
125 run_history: &'a mut RunHistory,
126 cli_settings: &'a mut CLISettings,
127 default_runtime_settings: &'a mut RuntimeSettings,
128 session_state: &'a mut CliSessionState,
129 ) -> Self {
130 Self {
131 state,
132 run_history,
133 cli_settings,
134 default_runtime_settings,
135 session_state,
136 }
137 }
138
139 pub fn execute_command(&mut self, command: CommandHistory) -> Result<CommandExecution> {
152 let prepared = PreparedCommand::prepare(command, self.run_history, 1)?;
153 self.execute_prepared(prepared, HistoryMode::Record)
154 }
155
156 pub fn replay_run_history(&mut self) -> Result<ControlFlow<SaveState>> {
161 self.run_history
162 .apply_session_settings(self.cli_settings, self.default_runtime_settings)?;
163 let prepared = prepare_command_histories_with_context(
164 &self.run_history.commands.clone(),
165 self.run_history,
166 1,
167 "run history",
168 )?;
169 self.execute_prepared_commands(prepared, HistoryMode::Suppress)
170 }
171
172 pub fn apply_boot_run_history(
173 &mut self,
174 boot_run_history: &RunHistory,
175 effective_boot_run_history: &RunHistory,
176 booted_existing_state: bool,
177 ) -> Result<ControlFlow<SaveState>> {
178 let interactive_prompt_available = io::stdin().is_terminal() && io::stdout().is_terminal();
179 self.apply_boot_run_history_with_conflict_resolver(
180 boot_run_history,
181 effective_boot_run_history,
182 booted_existing_state,
183 |conflicting_blocks| {
184 if interactive_prompt_available {
185 return prompt_command_block_conflict_override(conflicting_blocks);
186 }
187
188 Err(eyre!(
189 "{}. Cannot continue non-interactively; rerun in an interactive terminal or align the command block definitions.",
190 format_command_block_conflict_message(conflicting_blocks)
191 ))
192 },
193 )
194 }
195
196 pub fn apply_boot_run_history_with_conflict_resolver<F>(
197 &mut self,
198 boot_run_history: &RunHistory,
199 effective_boot_run_history: &RunHistory,
200 booted_existing_state: bool,
201 mut resolve_conflicts: F,
202 ) -> Result<ControlFlow<SaveState>>
203 where
204 F: FnMut(&[String]) -> Result<bool>,
205 {
206 boot_run_history.validate()?;
207
208 if booted_existing_state
209 && !self
210 .run_history
211 .frozen_boot_settings_match(boot_run_history)
212 {
213 let warning = format!(
214 "Boot card settings differ from the frozen settings stored in {}. This session is forced into --read-only-state. Line up cli_settings.global and default_runtime_settings in the boot card with the frozen settings in run.toml if you want to boot this state without read-only mode.",
215 self.cli_settings.state.folder.join("run.toml").display()
216 );
217 self.cli_settings
218 .session
219 .force_read_only_state(ReadOnlyStateOrigin::BootSettingsMismatch);
220 self.cli_settings
221 .session
222 .startup_warnings
223 .push(warning.clone());
224 warn!("{warning}");
225 }
226 if !booted_existing_state {
227 self.run_history.freeze_boot_settings_from(boot_run_history);
228 }
229
230 let mut merged_history = self.run_history.clone();
231 let conflicting_blocks = merged_history
232 .conflicting_command_block_names(&effective_boot_run_history.command_blocks);
233 let overwrite_conflicting_blocks = if conflicting_blocks.is_empty() {
234 false
235 } else if self.cli_settings.session.read_only_state {
236 if self
237 .cli_settings
238 .session
239 .is_read_only_due_to_boot_settings_mismatch()
240 {
241 let warning = format!(
242 "{}. The boot card settings differ from the frozen settings stored in {}; this session remains in --read-only-state for reproducibility, but will use the boot card command block definitions for this process only.",
243 format_command_block_conflict_message(&conflicting_blocks),
244 self.cli_settings.state.folder.join("run.toml").display()
245 );
246 self.cli_settings
247 .session
248 .startup_warnings
249 .push(warning.clone());
250 warn!("{warning}");
251 true
252 } else {
253 return Err(eyre!(
254 "{}. Cannot proceed while --read-only-state is enabled.",
255 format_command_block_conflict_message(&conflicting_blocks)
256 ));
257 }
258 } else if resolve_conflicts(&conflicting_blocks)? {
259 true
260 } else {
261 info!(
262 "{}",
263 "Boot run card application cancelled by user.".yellow()
264 );
265 return Ok(ControlFlow::Break(SaveState::default()));
266 };
267 merged_history.merge_command_blocks_with_overwrite(
268 &effective_boot_run_history.command_blocks,
269 overwrite_conflicting_blocks,
270 )?;
271
272 for block in &effective_boot_run_history.command_blocks {
273 let block_context = format!("command block '{}'", block.name);
274 let _ = prepare_command_histories_with_context(
275 &block.commands,
276 &merged_history,
277 2,
278 &block_context,
279 )?;
280 }
281
282 let prepared = prepare_command_histories_with_context(
283 &effective_boot_run_history.commands,
284 &merged_history,
285 1,
286 "boot",
287 )?;
288
289 self.run_history.merge_command_blocks_with_overwrite(
290 &effective_boot_run_history.command_blocks,
291 overwrite_conflicting_blocks,
292 )?;
293 effective_boot_run_history
294 .apply_session_settings(self.cli_settings, self.default_runtime_settings)?;
295 self.execute_prepared_commands(prepared, HistoryMode::Record)
296 }
297
298 pub fn has_pending_commands_block(&self) -> bool {
299 self.session_state.pending_commands_block.is_some()
300 }
301
302 pub fn pending_commands_block_name(&self) -> Option<String> {
303 self.session_state
304 .pending_commands_block
305 .as_ref()
306 .map(|pending| pending.name.clone())
307 }
308
309 pub fn prompt_state_label(&self) -> String {
310 self.cli_settings.state.prompt_label()
311 }
312
313 pub fn current_commands_block_names(&self) -> Vec<String> {
314 self.run_history
315 .command_blocks
316 .iter()
317 .map(|block| block.name.clone())
318 .collect()
319 }
320
321 pub fn current_process_entries(&self) -> Vec<ProcessCompletionEntry> {
322 self.state
323 .process_list
324 .processes
325 .iter()
326 .enumerate()
327 .map(|(id, process)| ProcessCompletionEntry {
328 id,
329 name: process.definition.folder_name.clone(),
330 kind: match &process.collection {
331 ProcessCollection::Amplitudes(_) => ProcessKind::Amplitude,
332 ProcessCollection::CrossSections(_) => ProcessKind::CrossSection,
333 },
334 integrand_names: process
335 .collection
336 .get_integrand_names()
337 .into_iter()
338 .map(str::to_string)
339 .collect(),
340 })
341 .collect()
342 }
343
344 pub fn current_integrand_detail_entries(&self) -> Vec<IntegrandDetailCompletionEntry> {
345 self.state
346 .process_list
347 .processes
348 .iter()
349 .enumerate()
350 .flat_map(|(process_id, process)| {
351 process
352 .collection
353 .get_integrand_names()
354 .into_iter()
355 .filter_map(move |integrand_name| {
356 let integrand_name = integrand_name.to_string();
357 let info = self
358 .state
359 .get_integrand_info(
360 Some(&ProcessRef::Id(process_id)),
361 Some(&integrand_name),
362 )
363 .ok()?;
364 let mut master_graph_names = info
365 .graph_groups
366 .iter()
367 .filter_map(|group| {
368 group
369 .graphs
370 .iter()
371 .find(|graph| graph.is_master)
372 .map(|graph| graph.name.clone())
373 })
374 .collect::<Vec<_>>();
375 master_graph_names.sort();
376 master_graph_names.dedup();
377
378 let mut categories = vec![
379 "generation".to_string(),
380 "orientation".to_string(),
381 "loop_momentum_basis".to_string(),
382 ];
383 if info.kind == IntegrandKind::CrossSection {
384 categories.push("cuts".to_string());
385 }
386 let signature_inventory =
387 integrand_signature_inventory(&process.collection, &integrand_name);
388 let amplitude_signature_inventory = amplitude_graph_signature_inventory(
389 &process.collection,
390 &integrand_name,
391 &self.state.model,
392 &process.definition,
393 &self.cli_settings.global.generation,
394 );
395 let raised_cut_signature_inventory = raised_cut_signature_inventory(
396 &process.collection,
397 &integrand_name,
398 &self.state.model,
399 &process.definition,
400 &self.cli_settings.global.generation,
401 );
402
403 Some(IntegrandDetailCompletionEntry {
404 process_name: process.definition.folder_name.clone(),
405 integrand_name,
406 kind: info.kind,
407 master_graph_names,
408 raised_all_signatures: signature_inventory.raised_all,
409 raised_massive_signatures: signature_inventory.raised_massive,
410 raised_massless_signatures: signature_inventory.raised_massless,
411 cycle_signatures: signature_inventory.cycles,
412 amplitude_raised_all_signatures: amplitude_signature_inventory
413 .raised_all,
414 amplitude_raised_massive_signatures: amplitude_signature_inventory
415 .raised_massive,
416 amplitude_raised_massless_signatures: amplitude_signature_inventory
417 .raised_massless,
418 amplitude_cycle_signatures: amplitude_signature_inventory.cycles,
419 raised_cut_all_signatures: raised_cut_signature_inventory.all,
420 raised_cut_massive_signatures: raised_cut_signature_inventory.massive,
421 raised_cut_massless_signatures: raised_cut_signature_inventory.massless,
422 categories,
423 })
424 })
425 .collect::<Vec<_>>()
426 })
427 .collect()
428 }
429
430 pub fn current_ir_profile_entries(&self) -> Vec<IrProfileCompletionEntry> {
431 self.state
432 .process_list
433 .processes
434 .iter()
435 .filter_map(|process| {
436 let ProcessCollection::CrossSections(cross_sections) = &process.collection else {
437 return None;
438 };
439
440 Some(
441 cross_sections
442 .iter()
443 .filter_map(|(integrand_name, cross_section)| {
444 let ProcessIntegrand::CrossSection(integrand) =
445 cross_section.integrand.as_ref()?
446 else {
447 return None;
448 };
449 let completion_entries = integrand.ir_profile_completion_entries();
450 let mut graph_names = completion_entries
451 .iter()
452 .map(|(graph_name, _)| graph_name.clone())
453 .collect::<Vec<_>>();
454 graph_names.sort();
455 graph_names.dedup();
456
457 let mut graph_limit_entries = completion_entries
458 .into_iter()
459 .flat_map(|(graph_name, limits)| {
460 limits
461 .into_iter()
462 .map(move |limit| format!("{graph_name} {limit}"))
463 })
464 .collect::<Vec<_>>();
465 graph_limit_entries.sort();
466 graph_limit_entries.dedup();
467
468 Some(IrProfileCompletionEntry {
469 process_name: process.definition.folder_name.clone(),
470 integrand_name: integrand_name.clone(),
471 graph_names,
472 graph_limit_entries,
473 })
474 })
475 .collect::<Vec<_>>(),
476 )
477 })
478 .flatten()
479 .collect()
480 }
481
482 pub(crate) fn current_process_settings_entries(&self) -> Vec<ProcessSettingsCompletionEntry> {
483 let mut entries = Vec::new();
484
485 for (process_id, process) in self.state.process_list.processes.iter().enumerate() {
486 for integrand_name in process.collection.get_integrand_names() {
487 let Ok(resolved) = process.get_integrand(integrand_name) else {
488 continue;
489 };
490 let Some(integrand) = resolved.integrand else {
491 continue;
492 };
493 let Ok(serialized) = serialize_runtime_named_settings(integrand.get_settings())
494 else {
495 continue;
496 };
497
498 entries.push(ProcessSettingsCompletionEntry {
499 process_id,
500 process_name: process.definition.folder_name.clone(),
501 integrand_name: integrand_name.to_string(),
502 quantities: serialized.quantities,
503 observables: serialized.observables,
504 selectors: serialized.selectors,
505 });
506 }
507 }
508
509 entries.sort_by(|left, right| {
510 left.process_id
511 .cmp(&right.process_id)
512 .then_with(|| left.integrand_name.cmp(&right.integrand_name))
513 });
514 entries
515 }
516
517 pub fn current_model_parameter_entries(
518 &self,
519 ) -> Vec<crate::repl::ModelParameterCompletionEntry> {
520 let mut entries = self
521 .state
522 .model_parameters
523 .keys()
524 .filter_map(|name| {
525 self.state
526 .model
527 .get_parameter_opt(name.to_string())
528 .map(|parameter| crate::repl::ModelParameterCompletionEntry {
529 name: name.to_string(),
530 parameter_type: parameter.parameter_type.clone(),
531 })
532 })
533 .collect::<Vec<_>>();
534 entries.sort_by(|left, right| left.name.cmp(&right.name));
535 entries
536 }
537
538 pub fn current_model_particle_names(&self) -> Vec<String> {
539 let mut names = self
540 .state
541 .model
542 .particles
543 .iter()
544 .flat_map(|particle| [particle.name.to_string(), particle.antiname.to_string()])
545 .collect::<Vec<_>>();
546 names.sort();
547 names.dedup();
548 names
549 }
550
551 pub fn current_model_select_particle_names(&self) -> Vec<String> {
552 let mut names = self
553 .state
554 .model
555 .particles
556 .iter()
557 .filter(|particle| particle.pdg_code > 0)
558 .map(|particle| particle.name.to_string())
559 .collect::<Vec<_>>();
560 names.sort();
561 names.dedup();
562 names
563 }
564
565 pub fn current_model_coupling_names(&self) -> Vec<String> {
566 let mut names = self
567 .state
568 .model
569 .orders
570 .iter()
571 .map(|order| order.name.to_string())
572 .collect::<Vec<_>>();
573 names.sort();
574 names.dedup();
575 names
576 }
577
578 pub fn current_model_vertices(&self) -> Vec<ModelVertexCompletionEntry> {
579 let mut entries = self
580 .state
581 .model
582 .vertex_rules
583 .iter()
584 .map(|vertex_rule| ModelVertexCompletionEntry {
585 name: vertex_rule.0.name.to_string(),
586 particles: vertex_rule
587 .0
588 .particles
589 .iter()
590 .map(|particle| particle.name.to_string())
591 .collect(),
592 })
593 .collect::<Vec<_>>();
594 entries.sort_by(|left, right| left.name.cmp(&right.name));
595 entries.dedup_by(|left, right| left.name == right.name);
596 entries
597 }
598
599 pub fn run_history_toml(&self) -> Result<String> {
600 self.run_history
601 .to_toml_string(self.cli_settings.try_strings)
602 }
603
604 pub fn global_settings_toml(&self) -> Result<String> {
605 render_smart_toml(self.cli_settings)
606 }
607
608 pub fn active_command_blocks(&self) -> BTreeMap<String, Vec<String>> {
609 self.run_history
610 .command_blocks
611 .iter()
612 .map(|block| {
613 (
614 block.name.clone(),
615 block.commands.iter().map(display_command).collect(),
616 )
617 })
618 .collect()
619 }
620
621 pub fn dismiss_pending_commands_block(&mut self, trigger: &str) -> bool {
622 let Some(pending) = self.session_state.pending_commands_block.take() else {
623 return false;
624 };
625
626 info!(
627 "{} {} {} {}",
628 "Dismissing command block definition".blue(),
629 pending.name.green(),
630 "after".blue(),
631 trigger.green()
632 );
633 true
634 }
635
636 fn execute_prepared_commands(
637 &mut self,
638 commands: Vec<PreparedCommand>,
639 history_mode: HistoryMode,
640 ) -> Result<ControlFlow<SaveState>> {
641 for command in commands {
642 let execution = self.execute_prepared(command, history_mode)?;
643 if let ControlFlow::Break(save_state) = execution.flow {
644 return Ok(ControlFlow::Break(save_state));
645 }
646 }
647 Ok(ControlFlow::Continue(()))
648 }
649
650 fn execute_prepared(
651 &mut self,
652 command: PreparedCommand,
653 history_mode: HistoryMode,
654 ) -> Result<CommandExecution> {
655 match command {
656 PreparedCommand::Plain(command) => self.execute_plain(command, history_mode),
657 PreparedCommand::Run { command, plan } => self.execute_run(command, plan, history_mode),
658 }
659 }
660
661 fn execute_plain(
662 &mut self,
663 command: CommandHistory,
664 history_mode: HistoryMode,
665 ) -> Result<CommandExecution> {
666 if self.session_state.pending_commands_block.is_some() {
667 return self.handle_recording_mode(command);
668 }
669
670 let display_text = display_command(&command);
671 info!("{} {}", "Running command".blue(), display_text.green());
672
673 let record = match &command.command {
674 Commands::StartCommandsBlock(start) => {
675 self.start_commands_block(start.clone())?;
676 false
677 }
678 Commands::FinishCommandsBlock => {
679 self.finish_commands_block()?;
680 false
681 }
682 _ => {
683 let execution = command.command.clone().run(
684 self.state,
685 self.run_history,
686 self.cli_settings,
687 self.default_runtime_settings,
688 )?;
689 if let ControlFlow::Break(save_state) = execution.flow {
690 if history_mode == HistoryMode::Record {
691 self.record_command(&command);
692 }
693 return Ok(CommandExecution::break_with(save_state));
694 }
695 let output = execution.output;
696 if history_mode == HistoryMode::Record {
697 self.record_command(&command);
698 }
699 return Ok(CommandExecution::continue_with(output));
700 }
701 };
702
703 if record {
704 self.record_command(&command);
705 }
706
707 Ok(CommandExecution::continue_without_output())
708 }
709
710 fn execute_run(
711 &mut self,
712 command: CommandHistory,
713 plan: PreparedRun,
714 history_mode: HistoryMode,
715 ) -> Result<CommandExecution> {
716 if self.session_state.pending_commands_block.is_some() {
717 return self.handle_recording_mode(command);
718 }
719
720 if plan.is_empty() {
721 return Ok(CommandExecution::continue_without_output());
722 }
723
724 let display_text = display_command(&command);
725 info!("{} {}", "Running command".blue(), display_text.green());
726
727 for block in plan.blocks {
728 info!("{} {}", "Starting command block".blue(), block.name.green());
729 if let ControlFlow::Break(save_state) =
730 self.execute_prepared_commands(block.commands, HistoryMode::Suppress)?
731 {
732 if history_mode == HistoryMode::Record {
733 self.record_command(&command);
734 }
735 return Ok(CommandExecution::break_with(save_state));
736 }
737 }
738
739 if !plan.commands.is_empty() {
740 info!(
741 "{} {}",
742 "Starting commands supplied with".blue(),
743 "--commands/-c".green()
744 );
745 }
746
747 if let ControlFlow::Break(save_state) =
748 self.execute_prepared_commands(plan.commands, HistoryMode::Suppress)?
749 {
750 if history_mode == HistoryMode::Record {
751 self.record_command(&command);
752 }
753 return Ok(CommandExecution::break_with(save_state));
754 }
755
756 if history_mode == HistoryMode::Record {
757 self.record_command(&command);
758 }
759
760 Ok(CommandExecution::continue_without_output())
761 }
762
763 fn handle_recording_mode(&mut self, command: CommandHistory) -> Result<CommandExecution> {
764 match command.command.clone() {
765 Commands::StartCommandsBlock(_) => Err(eyre!(
766 "Cannot start a new command block definition before finishing the current one"
767 )),
768 Commands::FinishCommandsBlock => {
769 self.finish_commands_block()?;
770 Ok(CommandExecution::continue_without_output())
771 }
772 Commands::Quit(_) => {
773 self.dismiss_pending_commands_block("quit");
774 Ok(CommandExecution::continue_without_output())
775 }
776 _ => {
777 let stored = normalize_command_history(&command);
778 let pending = self
779 .session_state
780 .pending_commands_block
781 .as_mut()
782 .expect("recording mode requires a pending block");
783 pending.commands.push(stored);
784 Ok(CommandExecution::continue_without_output())
785 }
786 }
787 }
788
789 fn start_commands_block(&mut self, start: StartCommandsBlock) -> Result<()> {
790 if self.session_state.pending_commands_block.is_some() {
791 return Err(eyre!(
792 "Cannot start a new command block definition before finishing the current one"
793 ));
794 }
795
796 if self.run_history.command_block(&start.name).is_some() {
797 info!(
798 "{} {}",
799 "Overwriting existing command block definition".blue(),
800 start.name.green()
801 );
802 } else {
803 info!(
804 "{} {}",
805 "Defining new command block definition".blue(),
806 start.name.green()
807 );
808 }
809
810 self.session_state.pending_commands_block = Some(PendingCommandsBlock {
811 name: start.name,
812 commands: Vec::new(),
813 });
814 Ok(())
815 }
816
817 fn finish_commands_block(&mut self) -> Result<()> {
818 let Some(pending) = self.session_state.pending_commands_block.take() else {
819 return Err(eyre!(
820 "No command block definition is currently being recorded"
821 ));
822 };
823
824 let block = CommandsBlock {
825 name: pending.name.clone(),
826 commands: pending.commands,
827 };
828
829 if let Some(existing_index) = self
830 .run_history
831 .command_blocks
832 .iter()
833 .position(|existing| existing.name == pending.name)
834 {
835 self.run_history.command_blocks[existing_index] = block;
836 } else {
837 self.run_history.command_blocks.push(block);
838 }
839
840 self.run_history.validate()?;
841 Ok(())
842 }
843
844 fn record_command(&mut self, command: &CommandHistory) {
845 if let Some(normalized) = normalize_persisted_command_history(command) {
846 self.run_history
847 .push_with_raw(normalized.command, normalized.raw_string);
848 }
849 }
850}
851
852fn integrand_signature_inventory(
853 collection: &ProcessCollection,
854 integrand_name: &str,
855) -> GraphSelectionSignatureInventory {
856 match collection {
857 ProcessCollection::Amplitudes(amplitudes) => amplitudes
858 .get(integrand_name)
859 .map(|amplitude| {
860 GraphSelectionSignatureInventory::from_master_graphs(
861 amplitude.graph_group_structure.iter().map(|group| {
862 let master_graph_id = group
863 .into_iter()
864 .next()
865 .expect("graph group should contain a master graph");
866 &litude.graphs[master_graph_id].graph
867 }),
868 )
869 })
870 .unwrap_or_else(GraphSelectionSignatureInventory::empty),
871 ProcessCollection::CrossSections(cross_sections) => cross_sections
872 .get(integrand_name)
873 .map(|cross_section| {
874 GraphSelectionSignatureInventory::from_master_graphs(
875 cross_section.graph_group_structure.iter().map(|group| {
876 let master_graph_id = group
877 .into_iter()
878 .next()
879 .expect("graph group should contain a master graph");
880 &cross_section.supergraphs[master_graph_id].graph
881 }),
882 )
883 })
884 .unwrap_or_else(GraphSelectionSignatureInventory::empty),
885 }
886}
887
888fn amplitude_graph_signature_inventory(
889 collection: &ProcessCollection,
890 integrand_name: &str,
891 model: &Model,
892 process_definition: &ProcessDefinition,
893 generation_settings: &GenerationSettings,
894) -> GraphSelectionSignatureInventory {
895 let ProcessCollection::CrossSections(cross_sections) = collection else {
896 return GraphSelectionSignatureInventory::empty();
897 };
898 cross_sections
899 .get(integrand_name)
900 .map(|cross_section| {
901 cross_section
902 .amplitude_graph_signature_inventory(model, process_definition, generation_settings)
903 .unwrap_or_else(|_| GraphSelectionSignatureInventory::empty())
904 })
905 .unwrap_or_else(GraphSelectionSignatureInventory::empty)
906}
907
908fn raised_cut_signature_inventory(
909 collection: &ProcessCollection,
910 integrand_name: &str,
911 model: &Model,
912 process_definition: &ProcessDefinition,
913 generation_settings: &GenerationSettings,
914) -> RaisedCutSignatureInventory {
915 let ProcessCollection::CrossSections(cross_sections) = collection else {
916 return RaisedCutSignatureInventory::empty();
917 };
918 cross_sections
919 .get(integrand_name)
920 .map(|cross_section| {
921 cross_section
922 .raised_cut_signature_inventory(model, process_definition, generation_settings)
923 .unwrap_or_else(|_| RaisedCutSignatureInventory::empty())
924 })
925 .unwrap_or_else(RaisedCutSignatureInventory::empty)
926}
927
928fn normalize_persisted_command_history(command: &CommandHistory) -> Option<CommandHistory> {
929 match &command.command {
930 Commands::Quit(_) | Commands::StartCommandsBlock(_) | Commands::FinishCommandsBlock => None,
931 Commands::Run(run) => normalize_persisted_run_history(command, run),
932 _ => Some(normalize_command_history(command)),
933 }
934}
935
936fn normalize_persisted_run_history(
937 command: &CommandHistory,
938 run: &crate::commands::Run,
939) -> Option<CommandHistory> {
940 let inline_commands = run
941 .parse_inline_commands()
942 .expect("persisted run commands should always have parseable inline commands");
943 let persisted_inline_commands = inline_commands
944 .iter()
945 .filter_map(normalize_persisted_command_history)
946 .collect::<Vec<_>>();
947
948 let persisted_run = crate::commands::Run {
949 block_names: run.block_names.clone(),
950 commands: (!persisted_inline_commands.is_empty()).then(|| {
951 persisted_inline_commands
952 .iter()
953 .map(persisted_command_raw_string)
954 .collect::<Vec<_>>()
955 .join("; ")
956 }),
957 };
958
959 if persisted_run.is_noop() {
960 return None;
961 }
962
963 if persisted_run == *run {
964 return Some(normalize_command_history(command));
965 }
966
967 let raw_string = persisted_run.canonical_raw_string();
968 Some(CommandHistory::new_with_raw(
969 Commands::Run(persisted_run),
970 raw_string,
971 ))
972}
973
974fn persisted_command_raw_string(command: &CommandHistory) -> String {
975 normalize_command_history(command)
976 .raw_string
977 .expect("persisted inline commands should remain representable as raw strings")
978}
979
980fn normalize_command_history(command: &CommandHistory) -> CommandHistory {
981 let raw_string = command
982 .raw_string
983 .as_deref()
984 .filter(|raw| raw_round_trips(raw, &command.command))
985 .map(str::to_string)
986 .or_else(|| match &command.command {
987 Commands::Run(run) => Some(run.canonical_raw_string()),
988 _ => None,
989 });
990
991 CommandHistory {
992 command: command.command.clone(),
993 raw_string,
994 }
995}
996
997fn raw_round_trips(raw: &str, command: &Commands) -> bool {
998 CommandHistory::from_raw_string(raw)
999 .map(|parsed| parsed.command == *command)
1000 .unwrap_or(false)
1001}
1002
1003pub(crate) fn display_command(command: &CommandHistory) -> String {
1004 let display = if let Some(raw_string) = normalize_command_history(command).raw_string {
1005 raw_string
1006 } else {
1007 format!("{:?}", command.command)
1008 };
1009
1010 truncate_multiline_display_command(&display)
1011}
1012
1013fn truncate_multiline_display_command(display: &str) -> String {
1014 let lines = display.lines().collect::<Vec<_>>();
1015 if lines.len() <= MAX_DISPLAY_COMMAND_LINES {
1016 return display.to_string();
1017 }
1018
1019 let mut truncated = lines[..MAX_DISPLAY_COMMAND_LINES].join("\n");
1020 truncated.push_str("\n[...]");
1021 truncated
1022}
1023
1024#[cfg(test)]
1025mod tests {
1026 use super::{display_command, MAX_DISPLAY_COMMAND_LINES};
1027 use crate::{commands::run::Run, commands::Commands, state::CommandHistory};
1028
1029 #[test]
1030 fn display_command_preserves_multiline_commands_up_to_line_limit() {
1031 let commands = (1..=MAX_DISPLAY_COMMAND_LINES)
1032 .map(|i| format!("cmd{i}"))
1033 .collect::<Vec<_>>()
1034 .join("\n");
1035 let command = CommandHistory::new(Commands::Run(Run {
1036 block_names: Vec::new(),
1037 commands: Some(commands.clone()),
1038 }));
1039
1040 let displayed = display_command(&command);
1041
1042 assert_eq!(displayed.lines().count(), MAX_DISPLAY_COMMAND_LINES);
1043 assert!(displayed.contains(&commands));
1044 assert!(!displayed.contains("[...]"));
1045 }
1046
1047 #[test]
1048 fn display_command_truncates_multiline_commands_beyond_line_limit() {
1049 let command = CommandHistory::new(Commands::Run(Run {
1050 block_names: Vec::new(),
1051 commands: Some("cmd1\ncmd2\ncmd3\ncmd4\ncmd5\ncmd6".to_string()),
1052 }));
1053
1054 let displayed = display_command(&command);
1055 let displayed_lines = displayed.lines().collect::<Vec<_>>();
1056
1057 assert_eq!(
1058 displayed_lines,
1059 vec!["run -c 'cmd1", "cmd2", "cmd3", "cmd4", "cmd5", "[...]"]
1060 );
1061 }
1062}