1use std::{
8 cell::{Cell, RefCell},
9 collections::{HashMap, HashSet},
10 rc::Rc,
11};
12
13use geop_core_math::geop_error::{GeopError, GeopResult};
14
15use crate::{
16 kept::{Digest, Made, digest_text},
17 operation_registry::OperationRegistry,
18 part::{Part, StepContext},
19 placed::{BuildCache, BuildKey},
20 program::{Program, ProgramStep},
21 reader::Reader,
22 target::Read,
23 values::{Settings, StateValues},
24 workspace::{Location, Workspace},
25};
26
27pub struct Runner {
32 pub registry: Rc<OperationRegistry>,
33 pub workspace: Option<Rc<dyn Workspace>>,
34 pub location: Location,
35 shared: Rc<Shared>,
37}
38
39#[derive(Default)]
41struct Shared {
42 cache: RefCell<BuildCache>,
43 counts: Cell<Counts>,
44 files: RefCell<HashMap<String, (u64, Option<Digest>)>>,
47}
48
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
52pub struct Counts {
53 pub makes: u64,
55 pub placed: u64,
57 pub builds: u64,
59 pub restored: u64,
62 pub kept_makes: u64,
65}
66
67pub(crate) fn quoted(names: &[&str]) -> String {
69 let quoted: Vec<String> = names.iter().map(|name| format!("`{name}`")).collect();
70 quoted.join(", ")
71}
72
73thread_local! {
74 static STEPS_RUN: Cell<usize> = const { Cell::new(0) };
75}
76
77pub fn steps_run() -> usize {
81 STEPS_RUN.get()
82}
83
84pub struct RunOutput {
86 pub step_errors: Vec<(String, GeopError)>,
89}
90
91impl Runner {
92 pub fn new(registry: OperationRegistry) -> Self {
95 Self::of(Rc::new(registry), None, Location::default())
96 }
97
98 pub fn of(
101 registry: Rc<OperationRegistry>,
102 workspace: Option<Rc<dyn Workspace>>,
103 location: Location,
104 ) -> Self {
105 Self {
106 registry,
107 workspace,
108 location,
109 shared: Rc::default(),
110 }
111 }
112
113 pub fn run(&self, part: &mut Part, program: &Program, upto: Option<&str>) -> RunOutput {
118 self.run_with(part, program, &Settings::new(), upto)
119 }
120
121 pub fn run_with(
123 &self,
124 part: &mut Part,
125 program: &Program,
126 inputs: &Settings,
127 upto: Option<&str>,
128 ) -> RunOutput {
129 self.build(part, &program.steps, inputs, &program.state, upto, None)
130 }
131
132 pub(crate) fn build(
136 &self,
137 part: &mut Part,
138 steps: &[ProgramStep],
139 inputs: &Settings,
140 state: &StateValues,
141 upto: Option<&str>,
142 outside: Option<Rc<Reader>>,
143 ) -> RunOutput {
144 let end = (upto.and_then(|upto| steps.iter().position(|step| step.id == upto)))
145 .unwrap_or(steps.len());
146 if part.ran_as(steps, end, inputs, state)
152 && part.outside_read.is_empty()
153 && (part.files_read.iter()).all(|(path, read)| self.revision(path) == *read)
154 {
155 return RunOutput {
156 step_errors: Vec::new(),
157 };
158 }
159 let order = steps.iter().map(|step| step.id.clone()).collect();
160 part.begin(order, inputs, state);
161 let mut step_errors = Vec::new();
162 let mut ids = HashSet::new();
163 for (at, step) in steps.iter().enumerate().take(end) {
164 crate::watch::report(&format!("building {}", step.id), at, end);
165 STEPS_RUN.set(STEPS_RUN.get() + 1);
166 part.begin_step(at);
167 let ran = match ids.insert(step.id.as_str()) {
168 true => self.run_step_of(part, step, at, outside.clone()),
169 false => Err(GeopError::new(format!(
172 "step `{}` appears twice in the program",
173 step.id
174 ))),
175 };
176 part.end_step(at);
177 if let Err(e) = ran {
178 let suppressed = part.suppressed_before(at);
181 let e = match suppressed.is_empty() {
182 true => e,
183 false => {
184 e.with_context(format!("built with {} suppressed", quoted(&suppressed)))
185 }
186 };
187 step_errors.push((step.id.clone(), e));
188 }
189 }
190 let failures = step_errors.clone();
191 part.end(failures);
192 part.ran_through(steps, end);
193 if self.location.within.is_empty() {
194 self.shared.cache.borrow_mut().sweep();
195 self.keep(part);
196 }
197 RunOutput { step_errors }
198 }
199
200 fn run_step_of(
205 &self,
206 part: &mut Part,
207 step: &ProgramStep,
208 at: usize,
209 outside: Option<Rc<Reader>>,
210 ) -> GeopResult<()> {
211 let mut ctx = StepContext::new(self, part, &step.id, at, outside);
212 if !ctx.active(step.active.as_deref())? {
213 part.suppress(&step.id);
214 return Ok(());
215 }
216 let operation = self.registry.get(&step.operation)?;
217 operation.run(&mut ctx, &step.args)
218 }
219
220 fn keep(&self, part: &Part) {
226 let Some(cache) = self
227 .workspace
228 .as_deref()
229 .and_then(|workspace| workspace.cache())
230 else {
231 return;
232 };
233 let mut seen = HashSet::new();
234 let mut parts = vec![(self.location.file.clone(), part)];
235 while let Some((path, part)) = parts.pop() {
236 for (_, _, placed) in part.placed() {
237 if seen.insert(Rc::as_ptr(&placed.part)) {
238 parts.push((placed.location.file.clone(), &placed.part));
239 }
240 }
241 cache.builds(&path, |builds| {
242 builds.order(part.order());
243 for (name, node) in part.kept_nodes() {
244 let (Some((derivation, digest)), Some(write), Ok(value)) =
245 (&node.kept, node.write, &node.value)
246 else {
247 continue;
248 };
249 if node.noted.replace(true) || builds.has(&node.step, name, derivation, *digest)
250 {
251 continue;
252 }
253 let value = value.clone();
254 builds.add(Made {
255 step: node.step.clone(),
256 target: name.to_string(),
257 kind: node.kind_name,
258 derivation: derivation.clone(),
259 digest: *digest,
260 write: Box::new(move |pool| write(&*value, pool)),
261 });
262 }
263 });
264 }
265 }
266
267 pub fn run_step(
271 &self,
272 part: &mut Part,
273 step: &str,
274 body: impl FnOnce(&mut StepContext) -> GeopResult<()>,
275 ) -> GeopResult<()> {
276 let mut order = part.order().to_vec();
277 if !order.iter().any(|id| id == step) {
278 order.push(step.to_string());
279 }
280 let at = order.iter().position(|id| id == step).unwrap_or_default();
281 let (inputs, state) = (part.inputs.clone(), part.state.clone());
282 part.begin(order, &inputs, &state);
283 part.begin_step(at);
284 let ran = body(&mut StepContext::new(self, part, step, at, None));
285 part.end_step(at);
286 let failures = (ran.as_ref().err())
287 .map(|e| (step.to_string(), e.clone()))
288 .into_iter()
289 .collect();
290 part.end(failures);
291 ran
292 }
293
294 pub fn counts(&self) -> Counts {
296 self.shared.counts.get()
297 }
298
299 pub fn cached_builds(&self) -> usize {
301 self.shared.cache.borrow().len()
302 }
303
304 pub(crate) fn count_make(&self, kept: bool) {
306 self.count(|counts| {
307 counts.makes += 1;
308 counts.kept_makes += u64::from(kept);
309 });
310 }
311
312 pub(crate) fn count_restored(&self) {
313 self.count(|counts| counts.restored += 1);
314 }
315
316 fn count(&self, add: impl FnOnce(&mut Counts)) {
317 let mut counts = self.shared.counts.get();
318 add(&mut counts);
319 self.shared.counts.set(counts);
320 }
321
322 pub fn nested(&self, location: Location) -> Runner {
325 Runner {
326 registry: self.registry.clone(),
327 workspace: self.workspace.clone(),
328 location,
329 shared: self.shared.clone(),
330 }
331 }
332
333 pub(crate) fn cached(&self, key: &BuildKey, holds: impl Fn(&Read) -> bool) -> Option<Rc<Part>> {
336 self.count(|counts| counts.placed += 1);
337 (self.shared.cache.borrow()).get(key, |path| self.revision(path), holds)
338 }
339
340 pub(crate) fn cache(&self, key: BuildKey, part: Rc<Part>) {
342 self.count(|counts| counts.builds += 1);
343 self.shared.cache.borrow_mut().insert(key, part);
344 }
345
346 pub fn revision(&self, path: &str) -> u64 {
348 (self.workspace.as_ref()).map_or(0, |workspace| workspace.revision(path))
349 }
350
351 pub(crate) fn file_digest(&self, path: &str) -> Option<Digest> {
355 let workspace = self.workspace.as_deref()?;
356 let revision = workspace.revision(path);
357 if let Some((read, digest)) = self.shared.files.borrow().get(path)
358 && *read == revision
359 {
360 return *digest;
361 }
362 let digest = workspace.digest(path).map(Digest);
363 (self.shared.files.borrow_mut()).insert(path.to_string(), (revision, digest));
364 digest
365 }
366
367 fn workspace(&self) -> GeopResult<&dyn Workspace> {
370 (self.workspace.as_deref()).ok_or_else(|| {
371 GeopError::new("this program is run without a workspace: there are no files to read")
372 })
373 }
374
375 pub fn files(&self) -> Vec<String> {
378 let Ok(workspace) = self.workspace() else {
379 return Vec::new();
380 };
381 let own = &self.location.file;
382 (workspace.list().into_iter())
383 .filter(|path| path != own)
384 .map(|path| workspace.relative(own, &path))
385 .collect()
386 }
387
388 pub fn read(&self, name: &str) -> GeopResult<(String, Rc<str>)> {
393 let (path, content) = self.content(name)?;
394 let text = match crate::workspace::text(&content) {
395 std::borrow::Cow::Borrowed(_) => content,
396 std::borrow::Cow::Owned(text) => text.into(),
397 };
398 Ok((path, text))
399 }
400
401 pub fn read_bytes(&self, name: &str) -> GeopResult<(String, Rc<[u8]>)> {
405 let (path, content) = self.content(name)?;
406 let bytes = crate::workspace::bytes(&content)
407 .map_err(|e| e.with_context(format!("reading the bytes of {path}")))?;
408 Ok((path, bytes.into()))
409 }
410
411 fn content(&self, name: &str) -> GeopResult<(String, Rc<str>)> {
414 let workspace = self.workspace()?;
415 let path = workspace.resolve(&self.location.file, name);
416 let content =
417 (workspace.read(&path)).map_err(|e| e.with_context(format!("reading {path}")))?;
418 let digest = Some(Digest(digest_text(&content)));
421 let revision = workspace.revision(&path);
422 (self.shared.files.borrow_mut()).insert(path.clone(), (revision, digest));
423 Ok((path, content))
424 }
425
426 pub fn program(&self, name: &str) -> GeopResult<(Rc<Program>, Location)> {
430 let workspace = self.workspace()?;
431 let path = workspace.resolve(&self.location.file, name);
432 let Location { file, within } = &self.location;
433 let within: Vec<String> = [within.clone(), vec![file.clone()]].concat();
434 if within.contains(&path) {
435 let cycle = within.iter().skip_while(|f| **f != path);
436 let chain: Vec<&str> = cycle.map(String::as_str).chain([path.as_str()]).collect();
437 return Err(GeopError::new(format!(
438 "{file} places {path:?}, which places it back: {} — files must not place \
439 themselves, directly or through others",
440 chain.join(" -> "),
441 )));
442 }
443 let loaded =
444 (workspace.program(&path)).map_err(|e| e.with_context(format!("placing {path}")))?;
445 Ok((loaded, Location { file: path, within }))
446 }
447}