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 successors: Vec<Vec<usize>>,
72    pub shell: Option<ShellKind>,
73    pub working_dir: PathBuf,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum PlanError {
78    UnknownTask(String),
79    HelperTask(String),
80    TaskUnavailable(String),
81    MissingRequiredParameter(String),
82    DependencyMissingRequiredParameter {
83        dependency: String,
84        parameter: String,
85    },
86    ConflictingDependencyArguments(String),
87    UnknownParameter {
88        task: String,
89        name: String,
90    },
91    DuplicateOverride(String),
92    CyclicDependency(String),
93    TooManyArguments {
94        task: String,
95        expected: usize,
96        got: usize,
97    },
98}
99
100impl fmt::Display for PlanError {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        match self {
103            Self::UnknownTask(task) => write!(f, "task '{task}' does not exist"),
104            Self::HelperTask(task) => {
105                write!(f, "helper task '{task}' cannot be run directly")
106            }
107            Self::TaskUnavailable(task) => {
108                write!(f, "task '{task}' is not available on this system")
109            }
110            Self::MissingRequiredParameter(name) => {
111                write!(f, "parameter '{{{{{name}}}}}' is required")
112            }
113            Self::DependencyMissingRequiredParameter {
114                dependency,
115                parameter,
116            } => write!(
117                f,
118                "dependency `{dependency}` requires parameter `{parameter}`\nprovide it with `{dependency}(\"value\")`"
119            ),
120            Self::ConflictingDependencyArguments(dependency) => write!(
121                f,
122                "dependency `{dependency}` is called with different arguments"
123            ),
124            Self::UnknownParameter { task, name } => {
125                write!(f, "task '{task}' has no parameter named '{name}'")
126            }
127            Self::DuplicateOverride(name) => {
128                write!(f, "parameter '{name}' was given more than once")
129            }
130            Self::CyclicDependency(path) => write!(f, "dependency loop: {path}"),
131            Self::TooManyArguments {
132                task,
133                expected,
134                got,
135            } => match expected {
136                0 => write!(f, "task '{task}' accepts no arguments, but got {got}"),
137                1 => write!(f, "task '{task}' accepts 1 argument, but got {got}"),
138                _ => write!(
139                    f,
140                    "task '{task}' accepts {expected} arguments, but got {got}"
141                ),
142            },
143        }
144    }
145}
146
147impl std::error::Error for PlanError {}
148
149/// Builds a dependency-ordered execution plan from semantic AST.
150///
151/// Args:
152/// document: Semantic AST used by the runtime.
153/// invocation: Requested task target.
154///
155/// Returns:
156/// Dependency-expanded execution plan in DAG order.
157pub fn build_execution_plan(document: &DocumentAst, invocation: Invocation<'_>) -> ExecutionPlan {
158    let working_dir = std::env::current_dir().unwrap_or_default();
159    try_build_execution_plan_in_dir(document, invocation, working_dir).unwrap_or_default()
160}
161
162/// Builds a dependency-ordered execution plan from semantic AST and returns planner errors.
163///
164/// Args:
165/// document: Semantic AST used by the runtime.
166/// invocation: Requested task target plus input bindings.
167///
168/// Returns:
169/// Dependency-expanded execution plan in DAG order.
170pub fn try_build_execution_plan(
171    document: &DocumentAst,
172    invocation: Invocation<'_>,
173) -> Result<ExecutionPlan, PlanError> {
174    let working_dir = std::env::current_dir().unwrap_or_default();
175    try_build_execution_plan_in_dir(document, invocation, working_dir)
176}
177
178/// Builds a dependency-ordered execution plan from semantic AST for an explicit working directory.
179///
180/// Args:
181/// document: Semantic AST used by the runtime.
182/// invocation: Requested task target plus input bindings.
183/// working_dir: Directory used by runtime execution.
184///
185/// Returns:
186/// Dependency-expanded execution plan in DAG order.
187pub fn try_build_execution_plan_in_dir(
188    document: &DocumentAst,
189    invocation: Invocation<'_>,
190    working_dir: PathBuf,
191) -> Result<ExecutionPlan, PlanError> {
192    let Invocation::Task {
193        target,
194        args,
195        overrides,
196    } = invocation;
197
198    let tasks = build_task_index(document);
199    let root = resolve_root_task(&tasks, target)?;
200    let globals = document
201        .directives
202        .iter()
203        .filter_map(|directive| match directive {
204            DirectiveAst::Variable { name, value, .. } => {
205                Some((name.to_string(), value.to_string()))
206            }
207            _ => None,
208        })
209        .collect::<std::collections::HashMap<_, _>>();
210    let overrides = merge_parameter_inputs(args, overrides, root, &globals)?;
211    let mut effective_globals = globals.clone();
212    for (name, value) in &overrides {
213        if globals.contains_key(name)
214            && !root.params.iter().any(|param| param.name.as_str() == name)
215        {
216            effective_globals.insert(name.clone(), value.clone());
217        }
218    }
219    let expanded = expand_execution_order(root, &overrides, &tasks, &effective_globals)?;
220
221    Ok(ExecutionPlan {
222        nodes: build_execution_nodes(expanded.ordered),
223        successors: expanded.successors,
224        shell: document_shell(document),
225        working_dir,
226    })
227}
228
229/// Resolves the concrete root task variant selected for the current environment.
230///
231/// Args:
232/// document: Semantic AST used by the runtime.
233/// target: Fully qualified task target.
234///
235/// Returns:
236/// The selected root task variant or a planner error when unavailable.
237pub fn select_root_task_variant<'a>(
238    document: &'a DocumentAst,
239    target: &str,
240) -> Result<&'a TaskAst, PlanError> {
241    resolve_root_task_in_document(document, target)
242}