Skip to main content

only_engine/
planner.rs

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