use serde::{Deserialize, Serialize};
use slash_lang::parser::ast::{Op, Program};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ExecutionStep {
Sequential { command: String },
ErrorRecovery { command: String },
Pipe { command: String },
PipeErr { command: String },
ContextAccumulation { commands: Vec<String> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionPlan {
pub steps: Vec<ExecutionStep>,
pub has_optional: bool,
pub has_error_recovery: bool,
}
impl ExecutionPlan {
pub fn new() -> Self {
Self {
steps: Vec::new(),
has_optional: false,
has_error_recovery: false,
}
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}
impl Default for ExecutionPlan {
fn default() -> Self {
Self::new()
}
}
pub fn program_to_plan(program: &Program) -> ExecutionPlan {
let mut plan = ExecutionPlan::new();
let mut has_error_recovery = false;
for (idx, pipeline) in program.pipelines.iter().enumerate() {
let prev_op = if idx > 0 {
program.pipelines[idx - 1].operator.as_ref()
} else {
None
};
if let Some(Op::Or) = prev_op {
has_error_recovery = true;
}
let mut optional_commands = Vec::new();
for (cmd_idx, cmd) in pipeline.commands.iter().enumerate() {
if cmd.optional {
optional_commands.push(cmd.name.clone());
plan.has_optional = true;
} else {
if !optional_commands.is_empty() {
plan.steps.push(ExecutionStep::ContextAccumulation {
commands: optional_commands.clone(),
});
optional_commands.clear();
}
let step = if cmd_idx == 0 && idx == 0 {
ExecutionStep::Sequential {
command: cmd.name.clone(),
}
} else if let Some(Op::Or) = prev_op {
ExecutionStep::ErrorRecovery {
command: cmd.name.clone(),
}
} else if cmd_idx > 0 {
match &pipeline.commands[cmd_idx - 1].pipe {
Some(Op::PipeErr) => ExecutionStep::PipeErr {
command: cmd.name.clone(),
},
Some(Op::Pipe) | Some(Op::And) | None => ExecutionStep::Pipe {
command: cmd.name.clone(),
},
_ => ExecutionStep::Sequential {
command: cmd.name.clone(),
},
}
} else {
ExecutionStep::Sequential {
command: cmd.name.clone(),
}
};
plan.steps.push(step);
}
}
if !optional_commands.is_empty() {
plan.steps.push(ExecutionStep::ContextAccumulation {
commands: optional_commands,
});
}
}
plan.has_error_recovery = has_error_recovery;
plan
}
#[cfg(test)]
mod tests {
use super::*;
use slash_lang::parser::parse;
#[test]
fn plan_simple_sequential() {
let prog = parse("/echo(hello) && /echo(world)").unwrap();
let plan = program_to_plan(&prog);
assert_eq!(plan.steps.len(), 2);
assert!(!plan.has_optional);
assert!(!plan.has_error_recovery);
}
#[test]
fn plan_error_recovery() {
let prog = parse("/false || /echo(fallback)").unwrap();
let plan = program_to_plan(&prog);
assert!(plan.has_error_recovery);
assert!(plan
.steps
.iter()
.any(|s| matches!(s, ExecutionStep::ErrorRecovery { .. })));
}
#[test]
fn plan_optional_commands() {
let prog = parse("/cmd? && /echo(done)").unwrap();
let plan = program_to_plan(&prog);
assert!(plan.has_optional);
assert!(plan
.steps
.iter()
.any(|s| matches!(s, ExecutionStep::ContextAccumulation { .. })));
}
#[test]
fn plan_to_json_roundtrip() {
let prog = parse("/echo(test)").unwrap();
let plan = program_to_plan(&prog);
let json = plan.to_json().unwrap();
let plan2 = ExecutionPlan::from_json(&json).unwrap();
assert_eq!(plan.steps.len(), plan2.steps.len());
assert_eq!(plan.has_optional, plan2.has_optional);
}
}