Skip to main content

geop_core_program/
runner.rs

1//! [`Runner`]: runs a program on a part ([`Runner::run`]) — every step's
2//! operation, in order, each declaring what it produces — and builds the
3//! parts it places, sharing equal builds. What a run made of the targets
4//! kept between processes it notes in its workspace's cache, to be written
5//! when the cache is next flushed (see [`crate::kept`]).
6
7use 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
27/// Runs programs on parts, with the operations of `registry`, reading
28/// what they place and import from `workspace` as the program in the file
29/// of `location` names it: `location.within` is the stack of files being
30/// built, which a file must not place again.
31pub struct Runner {
32    pub registry: Rc<OperationRegistry>,
33    pub workspace: Option<Rc<dyn Workspace>>,
34    pub location: Location,
35    /// What it shares with the runners of the parts it places.
36    shared: Rc<Shared>,
37}
38
39/// What a runner shares with the runners of the parts it places.
40#[derive(Default)]
41struct Shared {
42    cache: RefCell<BuildCache>,
43    counts: Cell<Counts>,
44    /// The digest of each file's text, by its path, and the revision it is
45    /// of: what a node that read the file was made from, in any process.
46    files: RefCell<HashMap<String, (u64, Option<Digest>)>>,
47}
48
49/// How much work a runner did: what a test of what runs counts, never a
50/// time.
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
52pub struct Counts {
53    /// The makes that ran (see [`StepContext::produce`]).
54    pub makes: u64,
55    /// The makes of placed parts among them.
56    pub placed: u64,
57    /// The parts placed that were built, not taken from the build cache.
58    pub builds: u64,
59    /// The nodes taken from what an earlier process kept, rather than made
60    /// (see [`crate::kept`]).
61    pub restored: u64,
62    /// The makes of targets kept between processes: what reopening a
63    /// program that did not change does none of.
64    pub kept_makes: u64,
65}
66
67/// `names`, each in backticks, joined: `` `a`, `b` ``.
68pub(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
77/// How many steps this thread has run, of every program and every part
78/// placed — each walked whether or not it made anything again: what a run
79/// costs at least, so a test can bound the runs an edit takes by a count.
80pub fn steps_run() -> usize {
81    STEPS_RUN.get()
82}
83
84/// How a run went.
85pub struct RunOutput {
86    /// The steps that failed, in program order, with why. A failed step does
87    /// not stop the run: later steps that do not depend on it still build.
88    pub step_errors: Vec<(String, GeopError)>,
89}
90
91impl Runner {
92    /// A runner of the operations of `registry`, of programs that read and
93    /// place nothing.
94    pub fn new(registry: OperationRegistry) -> Self {
95        Self::of(Rc::new(registry), None, Location::default())
96    }
97
98    /// A runner of the operations of `registry`, of the program in the file
99    /// of `location` of `workspace`, with a build cache of its own.
100    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    /// Runs the steps of `program` before the step `upto` — all of them, if
114    /// `None` — on `part`, with the program's state and no inputs. The
115    /// nodes of the steps after `upto` are kept for when they run again;
116    /// those of steps no longer in the program are dropped.
117    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    /// [`Runner::run`], its parameters set to `inputs`.
122    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    /// Runs `steps` before `upto` on `part`, with `inputs` and `state` —
133    /// placed in the part `outside` reads, if it is placed — and sweeps the
134    /// build cache after a run of a part not itself placed.
135    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        // Nothing changed since it last ran: every step would reuse what
147        // it made, and walking them all to find that is a run's cost — an
148        // editor runs its program several times an event of a drag. Not for
149        // a part that read the part placing it (`..`), which may have
150        // changed since.
151        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                // A step's id names what it produces: two steps of one id
170                // could not be told apart.
171                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                // What fails for what a step suppressed did not make says
179                // which steps those are.
180                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    /// Runs `step`, at position `at`, on `part` — unless it is suppressed
201    /// (see [`crate::program::ProgramStep::active`]): then it produces
202    /// nothing, and the steps after it see the part as the steps before it
203    /// left it.
204    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    /// Notes what `part`, built from the program it runs, and the parts
221    /// placed in it, however deep, hold of the targets kept between
222    /// processes in the cache of its workspace, each by the file of the
223    /// program that built it: what was made since the cache was written is
224    /// written when it is next flushed ([`crate::cache::Cache::flush`]).
225    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    /// Runs one step by hand, as the step `step` after those `part` ran,
268    /// `body` declaring what it produces: what a test of one operation
269    /// does. Fails as `body` does.
270    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    /// How much work it and the runners of the parts it placed did.
295    pub fn counts(&self) -> Counts {
296        self.shared.counts.get()
297    }
298
299    /// How many placed parts its build cache holds.
300    pub fn cached_builds(&self) -> usize {
301        self.shared.cache.borrow().len()
302    }
303
304    /// Counts a make, of a target `kept` between processes or not.
305    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    /// The runner of the part placed from the file of `location`: what an
323    /// editor edits a step of it with.
324    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    /// The part built from `key`, from the build cache: one whose reads of
334    /// the part it is placed in `holds` says hold.
335    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    /// Keeps `part`, built from `key`, in the build cache.
341    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    /// The revision of the file at `path` of its workspace: 0 without one.
347    pub fn revision(&self, path: &str) -> u64 {
348        (self.workspace.as_ref()).map_or(0, |workspace| workspace.revision(path))
349    }
350
351    /// The digest of the text of the file at `path` of its workspace, as it
352    /// is now: what a node that read it was made from, in any process. None
353    /// without a workspace, or for a file it does not have.
354    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    /// The workspace it reads from: an error, for a runner of programs that
368    /// read and place nothing.
369    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    /// The files the program it runs could read, as it names them: every
376    /// file but its own. None without a workspace.
377    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    /// The text of the file the program it runs names `name`, and its path:
389    /// a file of bytes read as UTF-8, what is not replaced (see
390    /// [`crate::workspace::text`]). A make reads it through its reader,
391    /// which records the read ([`crate::reader::Reader::read_file`]).
392    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    /// The bytes of the file the program it runs names `name`, and its
402    /// path (see [`crate::workspace::bytes`]): what a reader of a binary
403    /// format reads ([`crate::reader::Reader::read_bytes`]).
404    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    /// The content of the file the program it runs names `name`, as its
412    /// workspace holds it, and its path.
413    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        // What it was read as is what a node reading it was made from: its
419        // digest is taken now, not by reading it again.
420        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    /// The program in the file the program it runs names `name`, and where
427    /// it would be placed from here. Refuses a file placing the program it
428    /// runs, however deep: the files would place themselves.
429    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}