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