1use serde::{Deserialize, Serialize};
2use slash_lang::parser::ast::{Op, Program};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(tag = "type")]
7pub enum ExecutionStep {
8 Sequential { command: String },
10 ErrorRecovery { command: String },
12 Pipe { command: String },
14 PipeErr { command: String },
16 ContextAccumulation { commands: Vec<String> },
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ExecutionPlan {
30 pub steps: Vec<ExecutionStep>,
32 pub has_optional: bool,
34 pub has_error_recovery: bool,
36}
37
38impl ExecutionPlan {
39 pub fn new() -> Self {
41 Self {
42 steps: Vec::new(),
43 has_optional: false,
44 has_error_recovery: false,
45 }
46 }
47
48 pub fn to_json(&self) -> Result<String, serde_json::Error> {
50 serde_json::to_string_pretty(self)
51 }
52
53 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
55 serde_json::from_str(json)
56 }
57}
58
59impl Default for ExecutionPlan {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65pub fn program_to_plan(program: &Program) -> ExecutionPlan {
72 let mut plan = ExecutionPlan::new();
73 let mut has_error_recovery = false;
74
75 for (idx, pipeline) in program.pipelines.iter().enumerate() {
76 let prev_op = if idx > 0 {
79 program.pipelines[idx - 1].operator.as_ref()
80 } else {
81 None
82 };
83
84 if let Some(Op::Or) = prev_op {
85 has_error_recovery = true;
86 }
87
88 let mut optional_commands = Vec::new();
90
91 for (cmd_idx, cmd) in pipeline.commands.iter().enumerate() {
92 if cmd.optional {
93 optional_commands.push(cmd.name.clone());
95 plan.has_optional = true;
96 } else {
97 if !optional_commands.is_empty() {
99 plan.steps.push(ExecutionStep::ContextAccumulation {
100 commands: optional_commands.clone(),
101 });
102 optional_commands.clear();
103 }
104
105 let step = if cmd_idx == 0 && idx == 0 {
107 ExecutionStep::Sequential {
109 command: cmd.name.clone(),
110 }
111 } else if let Some(Op::Or) = prev_op {
112 ExecutionStep::ErrorRecovery {
114 command: cmd.name.clone(),
115 }
116 } else if cmd_idx > 0 {
117 match &pipeline.commands[cmd_idx - 1].pipe {
119 Some(Op::PipeErr) => ExecutionStep::PipeErr {
120 command: cmd.name.clone(),
121 },
122 Some(Op::Pipe) | Some(Op::And) | None => ExecutionStep::Pipe {
123 command: cmd.name.clone(),
124 },
125 _ => ExecutionStep::Sequential {
126 command: cmd.name.clone(),
127 },
128 }
129 } else {
130 ExecutionStep::Sequential {
132 command: cmd.name.clone(),
133 }
134 };
135
136 plan.steps.push(step);
137 }
138 }
139
140 if !optional_commands.is_empty() {
142 plan.steps.push(ExecutionStep::ContextAccumulation {
143 commands: optional_commands,
144 });
145 }
146 }
147
148 plan.has_error_recovery = has_error_recovery;
149 plan
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use slash_lang::parser::parse;
156
157 #[test]
158 fn plan_simple_sequential() {
159 let prog = parse("/echo(hello) && /echo(world)").unwrap();
160 let plan = program_to_plan(&prog);
161 assert_eq!(plan.steps.len(), 2);
162 assert!(!plan.has_optional);
163 assert!(!plan.has_error_recovery);
164 }
165
166 #[test]
167 fn plan_error_recovery() {
168 let prog = parse("/false || /echo(fallback)").unwrap();
169 let plan = program_to_plan(&prog);
170 assert!(plan.has_error_recovery);
171 assert!(plan
173 .steps
174 .iter()
175 .any(|s| matches!(s, ExecutionStep::ErrorRecovery { .. })));
176 }
177
178 #[test]
179 fn plan_optional_commands() {
180 let prog = parse("/cmd? && /echo(done)").unwrap();
181 let plan = program_to_plan(&prog);
182 assert!(plan.has_optional);
183 assert!(plan
185 .steps
186 .iter()
187 .any(|s| matches!(s, ExecutionStep::ContextAccumulation { .. })));
188 }
189
190 #[test]
191 fn plan_to_json_roundtrip() {
192 let prog = parse("/echo(test)").unwrap();
193 let plan = program_to_plan(&prog);
194 let json = plan.to_json().unwrap();
195 let plan2 = ExecutionPlan::from_json(&json).unwrap();
196 assert_eq!(plan.steps.len(), plan2.steps.len());
197 assert_eq!(plan.has_optional, plan2.has_optional);
198 }
199}