heron_rebuild_workflow/
workflow.rs

1use anyhow::{Context, Result};
2use std::path::{Path, PathBuf};
3
4use intern::{GetStr, InternStr};
5use syntax::ast;
6use util::{HashMap, Hasher, IdVec, PathEncodingError};
7
8use crate::{
9    branch::parse_compact_branch_str, AbstractTaskId, AbstractValueId, BranchSpec, Error, IdentId,
10    LiteralId, ModuleId, Plan, Task, Value, WorkflowStrings,
11};
12
13/// Used to initialize collections later in the process.
14#[derive(Debug, Default)]
15pub struct SizeHints {
16    pub max_inputs: u8,
17    pub max_outputs: u8,
18    pub max_params: u8,
19    pub max_vars: u8,
20}
21
22/// Contains all the information about a workflow,
23/// in a form that can be used to generate a traversal to run.
24#[derive(Debug)]
25pub struct Workflow {
26    /// All strings defined in the config file
27    pub strings: WorkflowStrings,
28    /// lookup global config values by name
29    config: HashMap<IdentId, AbstractValueId>,
30    /// all tasks defined in the config file
31    tasks: IdVec<AbstractTaskId, Task>,
32    /// all plans defined in the config file
33    plans: Vec<(IdentId, Plan)>,
34    /// all modules defined in the config file
35    modules: IdVec<ModuleId, LiteralId>,
36    /// all values, including global config values and task variables
37    values: IdVec<AbstractValueId, Value>,
38    /// sizes we'll use to allocate collections later
39    sizes: SizeHints,
40}
41
42impl Default for Workflow {
43    fn default() -> Self {
44        Self {
45            strings: WorkflowStrings::default(),
46            config: HashMap::with_capacity_and_hasher(64, Hasher::default()),
47            tasks: IdVec::with_capacity(16),
48            plans: Vec::with_capacity(8),
49            modules: IdVec::with_capacity(8),
50            values: IdVec::with_capacity(128),
51            sizes: SizeHints::default(),
52        }
53    }
54}
55
56impl Workflow {
57    /// Load the given ast representations of blocks into this `Workflow`.
58    /// `config_dir` is used to interpret relative paths to modules.
59    #[rustfmt::skip]
60    pub fn load(&mut self, blocks: Vec<ast::Item>, config_dir: &Path) -> Result<()> {
61        for block in blocks {
62            match block {
63                ast::Item::GlobalConfig(assts)  => self.add_config(assts)?,
64                ast::Item::Task(task)           => self.add_task(task)?,
65                ast::Item::Plan(plan)           => self.add_plan(plan)?,
66                ast::Item::Module(name, path)   => self.add_module(name, path, config_dir)?,
67                _ => {
68                    return Err(Error::Unsupported(
69                        "blocks other than config, task, plan, module".to_owned(),
70                    )
71                    .into())
72                }
73            }
74        }
75        Ok(())
76    }
77
78    /// Get a reference to size hints for initializing collections.
79    #[inline]
80    pub fn sizes(&self) -> &SizeHints {
81        &self.sizes
82    }
83
84    /// Get a string containing the path to the module with the given id.
85    #[inline]
86    pub fn get_module_path(&self, module: ModuleId) -> Result<&str> {
87        let lit_id = self.modules.get(module).ok_or(Error::ModuleNotFound(module))?;
88        self.strings.literals.get(*lit_id)
89    }
90
91    /// Get the task with the given id.
92    #[inline]
93    pub fn get_task(&self, task: AbstractTaskId) -> Result<&Task, Error> {
94        self.tasks.get(task).filter(|t| t.exists).ok_or(Error::TaskNotFound(task))
95    }
96
97    /// Get the value with the given id.
98    #[inline]
99    pub fn get_value(&self, value: AbstractValueId) -> Result<&Value, Error> {
100        self.values.get(value).ok_or(Error::ValueNotFound(value))
101    }
102
103    #[inline]
104    pub fn get_config_value(&self, ident: IdentId) -> Option<AbstractValueId> {
105        self.config.get(&ident).copied()
106    }
107
108    /// Total number of values defined (including task variables and config values).
109    #[inline]
110    pub fn num_values(&self) -> usize {
111        self.values.len()
112    }
113
114    /// Get a reference to the plan defined with the given identifier.
115    pub fn get_plan(&self, plan_name: IdentId) -> Result<&Plan, Error> {
116        for (k, plan) in &self.plans {
117            if *k == plan_name {
118                return Ok(plan);
119            }
120        }
121        Err(Error::PlanNotFound(plan_name))
122    }
123
124    /// Parse "compact" branch string (i.e. with "Baseline.baseline" standing in for baseline branches)
125    /// into a `BranchSpec`.
126    #[inline]
127    pub fn parse_compact_branch_str(&mut self, s: &str) -> Result<BranchSpec> {
128        parse_compact_branch_str(self, s)
129    }
130}
131
132// building the workflow /////////////
133impl Workflow {
134    fn add_config(&mut self, assignments: Vec<(&str, ast::Rhs)>) -> Result<()> {
135        for (lhs, rhs) in assignments {
136            let v = self.strings.create_value(lhs, rhs)?;
137            let vid = self.values.push(v);
138            let k = self.strings.idents.intern(lhs)?;
139            self.config.insert(k, vid);
140        }
141        Ok(())
142    }
143
144    fn add_task(&mut self, task: ast::TasklikeBlock) -> Result<()> {
145        let name_id = self.strings.tasks.intern(task.name)?;
146        let task = Task::create(task, &mut self.strings, &mut self.values)?;
147        self.update_sizes(&task);
148        // NB we have no easy, surefire way to tell if a task with the same
149        // name was added, so if that happens then the task will just be
150        // overwritten. Wd be nice to make that an error eventually.
151        self.tasks.insert(name_id, task);
152        Ok(())
153    }
154
155    fn update_sizes(&mut self, task: &Task) {
156        let num_inputs = task.vars.inputs.len() as u8;
157        let num_outputs = task.vars.outputs.len() as u8;
158        let num_params = task.vars.params.len() as u8;
159        let num_vars = num_inputs + num_outputs + num_params;
160        self.sizes.max_inputs = self.sizes.max_inputs.max(num_inputs);
161        self.sizes.max_outputs = self.sizes.max_outputs.max(num_outputs);
162        self.sizes.max_params = self.sizes.max_params.max(num_params);
163        self.sizes.max_vars = self.sizes.max_vars.max(num_vars);
164    }
165
166    fn add_plan(&mut self, plan: ast::Plan) -> Result<()> {
167        let plan_id = self.strings.idents.intern(plan.name)?;
168        let ast::Plan { cross_products, .. } = plan;
169
170        // the parser will catch this, but nice to have the error just in case
171        // that ever changes:
172        if cross_products.is_empty() {
173            return Err(Error::EmptyPlan(plan.name.to_owned()).into());
174        }
175
176        let plan = Plan::create(&mut self.strings, cross_products)
177            .with_context(|| format!("while creating AST for plan \"{}\"", plan.name))?;
178
179        // NB we don't use an IdVec bc plans use the idents table,
180        // so the vec would be very sparse. cd use a HashMap tho...
181        self.plans.push((plan_id, plan));
182        Ok(())
183    }
184
185    fn add_module(&mut self, name: &str, path: ast::Rhs, config_dir: &Path) -> Result<()> {
186        let id = self.strings.modules.intern(name)?;
187        if let ast::Rhs::Literal { val } = path {
188            let mut path = PathBuf::from(val);
189
190            if path.is_relative() {
191                path = config_dir.join(path);
192            }
193
194            if path.exists() {
195                path = path.canonicalize()?;
196            } else {
197                log::debug!(
198                    "Module path {:?} does not exist; this may cause errors later.",
199                    path
200                );
201            }
202            let path_str = path.to_str().ok_or(PathEncodingError)?;
203            let literal_id = self.strings.literals.intern(path_str)?;
204            self.modules.insert(id, literal_id);
205            Ok(())
206        } else {
207            Err(Error::Unsupported(format!(
208                "Module values other than literal strings (in module \"{}\")",
209                name
210            ))
211            .into())
212        }
213    }
214}