Skip to main content

only_engine/
planner.rs

1use std::fmt;
2use std::path::PathBuf;
3
4use only_semantic::{
5    DirectiveAst, DocumentAst, InterpolationAst, ShellKind, ShellSelection, TaskAst,
6};
7
8use crate::dag::expand_execution_order;
9use crate::resolve::{
10    build_execution_nodes, build_task_index, document_shell, merge_parameter_inputs,
11    resolve_root_task, resolve_root_task_in_document,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum Invocation<'a> {
16    Task {
17        target: &'a str,
18        args: Vec<&'a str>,
19        overrides: Vec<(&'a str, &'a str)>,
20    },
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ExecutionNode {
25    pub stage: usize,
26    pub name: String,
27    pub steps: Vec<ExecutionStep>,
28    pub params: Vec<PlanParam>,
29    pub result_params: Vec<PlanParam>,
30    pub pass: Option<String>,
31    pub fail: Option<String>,
32    pub shell: Option<ShellSelection>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum ExecutionStep {
37    Command {
38        source: String,
39        interpolations: Vec<InterpolationAst>,
40    },
41    CommandBlock {
42        source: String,
43        line_count: usize,
44        interpolations: Vec<InterpolationAst>,
45    },
46}
47
48impl ExecutionStep {
49    pub fn source(&self) -> &str {
50        match self {
51            Self::Command { source, .. } => source,
52            Self::CommandBlock { source, .. } => source,
53        }
54    }
55
56    pub fn is_block(&self) -> bool {
57        matches!(self, Self::CommandBlock { .. })
58    }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct PlanParam {
63    pub name: String,
64    pub default_value: Option<String>,
65    pub value: Option<String>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Default)]
69pub struct ExecutionPlan {
70    pub nodes: Vec<ExecutionNode>,
71    pub shell: Option<ShellKind>,
72    pub working_dir: PathBuf,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum PlanError {
77    UnknownTask(String),
78    HelperTask(String),
79    TaskUnavailable(String),
80    MissingRequiredParameter(String),
81    DependencyMissingRequiredParameter {
82        dependency: String,
83        parameter: String,
84    },
85    ConflictingDependencyArguments(String),
86    UnknownParameter {
87        task: String,
88        name: String,
89    },
90    DuplicateOverride(String),
91    CyclicDependency(String),
92    TooManyArguments {
93        task: String,
94        expected: usize,
95        got: usize,
96    },
97}
98
99impl fmt::Display for PlanError {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match self {
102            Self::UnknownTask(task) => write!(f, "task '{task}' does not exist"),
103            Self::HelperTask(task) => {
104                write!(f, "helper task '{task}' cannot be run directly")
105            }
106            Self::TaskUnavailable(task) => {
107                write!(f, "task '{task}' is not available on this system")
108            }
109            Self::MissingRequiredParameter(name) => {
110                write!(f, "parameter '{{{{{name}}}}}' is required")
111            }
112            Self::DependencyMissingRequiredParameter {
113                dependency,
114                parameter,
115            } => write!(
116                f,
117                "dependency `{dependency}` requires parameter `{parameter}`\nprovide it with `{dependency}(\"value\")`"
118            ),
119            Self::ConflictingDependencyArguments(dependency) => write!(
120                f,
121                "dependency `{dependency}` is called with different arguments"
122            ),
123            Self::UnknownParameter { task, name } => {
124                write!(f, "task '{task}' has no parameter named '{name}'")
125            }
126            Self::DuplicateOverride(name) => {
127                write!(f, "parameter '{name}' was given more than once")
128            }
129            Self::CyclicDependency(path) => write!(f, "dependency loop: {path}"),
130            Self::TooManyArguments {
131                task,
132                expected,
133                got,
134            } => match expected {
135                0 => write!(f, "task '{task}' accepts no arguments, but got {got}"),
136                1 => write!(f, "task '{task}' accepts 1 argument, but got {got}"),
137                _ => write!(
138                    f,
139                    "task '{task}' accepts {expected} arguments, but got {got}"
140                ),
141            },
142        }
143    }
144}
145
146impl std::error::Error for PlanError {}
147
148/// Builds a dependency-ordered execution plan from semantic AST.
149///
150/// Args:
151/// document: Semantic AST used by the runtime.
152/// invocation: Requested task target.
153///
154/// Returns:
155/// Dependency-expanded execution plan in DAG order.
156pub fn build_execution_plan(document: &DocumentAst, invocation: Invocation<'_>) -> ExecutionPlan {
157    let working_dir = std::env::current_dir().unwrap_or_default();
158    try_build_execution_plan_in_dir(document, invocation, working_dir).unwrap_or_default()
159}
160
161/// Builds a dependency-ordered execution plan from semantic AST and returns planner errors.
162///
163/// Args:
164/// document: Semantic AST used by the runtime.
165/// invocation: Requested task target plus input bindings.
166///
167/// Returns:
168/// Dependency-expanded execution plan in DAG order.
169pub fn try_build_execution_plan(
170    document: &DocumentAst,
171    invocation: Invocation<'_>,
172) -> Result<ExecutionPlan, PlanError> {
173    let working_dir = std::env::current_dir().unwrap_or_default();
174    try_build_execution_plan_in_dir(document, invocation, working_dir)
175}
176
177/// Builds a dependency-ordered execution plan from semantic AST for an explicit working directory.
178///
179/// Args:
180/// document: Semantic AST used by the runtime.
181/// invocation: Requested task target plus input bindings.
182/// working_dir: Directory used by runtime execution.
183///
184/// Returns:
185/// Dependency-expanded execution plan in DAG order.
186pub fn try_build_execution_plan_in_dir(
187    document: &DocumentAst,
188    invocation: Invocation<'_>,
189    working_dir: PathBuf,
190) -> Result<ExecutionPlan, PlanError> {
191    let Invocation::Task {
192        target,
193        args,
194        overrides,
195    } = invocation;
196
197    let tasks = build_task_index(document);
198    let root = resolve_root_task(&tasks, target)?;
199    let globals = document
200        .directives
201        .iter()
202        .filter_map(|directive| match directive {
203            DirectiveAst::Variable { name, value, .. } => {
204                Some((name.to_string(), value.to_string()))
205            }
206            _ => None,
207        })
208        .collect::<std::collections::HashMap<_, _>>();
209    let overrides = merge_parameter_inputs(args, overrides, root, &globals)?;
210    let mut effective_globals = globals.clone();
211    for (name, value) in &overrides {
212        if globals.contains_key(name)
213            && !root.params.iter().any(|param| param.name.as_str() == name)
214        {
215            effective_globals.insert(name.clone(), value.clone());
216        }
217    }
218    let ordered = expand_execution_order(root, &overrides, &tasks, &effective_globals)?;
219
220    Ok(ExecutionPlan {
221        nodes: build_execution_nodes(ordered),
222        shell: document_shell(document),
223        working_dir,
224    })
225}
226
227/// Resolves the concrete root task variant selected for the current environment.
228///
229/// Args:
230/// document: Semantic AST used by the runtime.
231/// target: Fully qualified task target.
232///
233/// Returns:
234/// The selected root task variant or a planner error when unavailable.
235pub fn select_root_task_variant<'a>(
236    document: &'a DocumentAst,
237    target: &str,
238) -> Result<&'a TaskAst, PlanError> {
239    resolve_root_task_in_document(document, target)
240}