Skip to main content

only_engine/
planner.rs

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