Skip to main content

ravenrs_sheet/engine/macros/
run.rs

1//! Running a macro.
2//!
3//! 1. **Gate**: every step's command goes through [`macro_policy`]. Denied commands stop the macro
4//!    before anything runs; commands that touch files must be listed in the macro's permissions;
5//!    a macro from a file someone else made, or one that touches files, runs only once the person
6//!    approves it (once per macro content, for the session).
7//! 2. **Inputs**: each declared input is bound to a range (given, its default, or the selection)
8//!    and checked against its shape and column types, with messages that name the cells.
9//! 3. **Footprint**: unless the macro may overwrite, it first runs on the real workbook in dry-run
10//!    mode, the changes are compared with `CellStore::diff`, and everything is put back. Cells it
11//!    would write outside its inputs that already hold data are conflicts, handled by the output
12//!    rule: ask, fail, overwrite, or shift the data down or right (and check again). The
13//!    footprint can miss a write (a dry run stops early at a file step or Solver, and RAND or NOW
14//!    can send the run another way than the dry run), so the run also checks each step as it goes.
15//! 4. **Run**: one undo step (`Session::grouped`); an error at any step rolls everything back and
16//!    names the step.
17//!
18//! The steps run on a small machine with an explicit frame stack, so the debugger can stop
19//! between any two steps and pick up again on a later frame.
20
21use std::collections::{BTreeSet, HashMap};
22use std::sync::Arc;
23
24use crate::base::{CellRef, RangeRef, Value};
25use crate::formula::adjust::Edit;
26use crate::model::{Cell, CompareOp, InputKind, MAX_MACRO_REF, Macro, OnConflict, RepeatOf, Sheet, Step, StepKind, Workbook};
27use serde::Serialize;
28use serde_json::{Value as Json, json};
29
30use super::policy::{Policy, macro_policy, previewable, writes_file};
31use super::resolve::{self, Env, Place, ResolveError};
32use crate::engine::{EngineError, Result, Selection, Session, Trust, UiRequest};
33
34/// The most steps one run executes (counting each pass through a Repeat).
35pub const MAX_EXECUTED_STEPS: u64 = 100_000;
36/// How long a run may take, in milliseconds. The web build has no clock; there only the step
37/// limit applies.
38pub const DEFAULT_TIME_LIMIT_MS: f64 = 60_000.0;
39/// The most cells an input may have for its content to be checked.
40pub const MAX_INPUT_CELLS: u64 = 1_000_000;
41/// The most bytes of parameters one run passes to its commands, all steps together: on the web
42/// build, where there is no clock, this and the step limit bound the work a run does.
43pub const MAX_PARAM_BYTES: u64 = 256 * 1024 * 1024;
44/// The most ranges a footprint lists per sheet (the counts stay exact).
45const MAX_LISTED_RANGES: usize = 500;
46
47/// An input bound to a range: name, where, whether its first row is a header.
48pub type Bound = (String, Place, bool);
49
50/// Where and why a step failed.
51#[derive(Clone, Debug, PartialEq, Serialize)]
52pub struct StepError {
53    /// The step's path: `3`, `2.1` inside a Repeat, `4.then.1` inside an If.
54    pub step: String,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub command: Option<String>,
57    pub message: String,
58    /// The step ran past the edge of the sheet.
59    #[serde(skip_serializing_if = "std::ops::Not::not")]
60    pub edge: bool,
61    /// The step would write over data and the conflict rule is ask: overwriting answers it.
62    #[serde(skip_serializing_if = "std::ops::Not::not")]
63    pub confirm: bool,
64    /// Files the run wrote before the step failed ("step 1 (file.exportCsv)"): rolling back the
65    /// workbook leaves them written.
66    #[serde(skip_serializing_if = "Vec::is_empty")]
67    pub wrote_files: Vec<String>,
68}
69
70impl StepError {
71    fn new(step: &str, command: Option<&str>, message: impl Into<String>) -> StepError {
72        StepError {
73            step: step.to_string(),
74            command: command.map(str::to_string),
75            message: message.into(),
76            edge: false,
77            confirm: false,
78            wrote_files: Vec::new(),
79        }
80    }
81
82    fn resolve(step: &str, command: Option<&str>, e: ResolveError) -> StepError {
83        let edge = matches!(e, ResolveError::Edge(_));
84        StepError { edge, ..StepError::new(step, command, e.message()) }
85    }
86
87    /// `Step 3 (cell.set): …`
88    pub fn describe(&self) -> String {
89        match &self.command {
90            Some(c) => format!("Step {} ({c}): {}", self.step, self.message),
91            None => format!("Step {}: {}", self.step, self.message),
92        }
93    }
94
95    pub fn into_engine(self, name: &str) -> EngineError {
96        let text = format!("Macro '{name}' stopped. {} {}", self.describe(), rolled_back(&self.wrote_files));
97        if self.confirm { EngineError::Confirm(text) } else { EngineError::Other(text) }
98    }
99}
100
101/// What a run that rolled back leaves: "Nothing was changed.", or the files it wrote before.
102pub fn rolled_back(wrote_files: &[String]) -> String {
103    match wrote_files {
104        [] => "Nothing was changed.".into(),
105        [one] => format!("The workbook was put back; the file {one} wrote stays."),
106        many => format!("The workbook was put back; the files {} wrote stay.", join_and(many)),
107    }
108}
109
110/// The size of `v` as JSON text, near enough to bound how much a run passes to its commands.
111fn json_bytes(v: &Json) -> u64 {
112    match v {
113        Json::String(t) => t.len() as u64 + 2,
114        Json::Array(a) => a.iter().map(json_bytes).fold(2, u64::saturating_add),
115        Json::Object(o) => o.iter().map(|(k, v)| (k.len() as u64 + 3).saturating_add(json_bytes(v))).fold(2, u64::saturating_add),
116        _ => 8,
117    }
118}
119
120// ---------------------------------------------------------------- gate
121
122/// Checks every enabled step against the policy: denied commands and undeclared file access are
123/// errors; otherwise returns what the person must approve (empty when nothing).
124pub fn gate(m: &Macro, trust: Trust) -> Result<Vec<String>> {
125    let mut asks = BTreeSet::new();
126    let mut err = None;
127    walk_enabled(&m.steps, "", &mut |path, id, params| {
128        if err.is_some() {
129            return;
130        }
131        match macro_policy(id, params, trust) {
132            Policy::Allow => {}
133            Policy::Ask(why) => {
134                asks.insert(why);
135            }
136            Policy::NeedsPermission(what) => {
137                if m.permissions.iter().any(|p| p == id) {
138                    asks.insert(format!("runs {id}, which {what}"));
139                } else {
140                    err = Some(format!("Step {path} ({id}) {what}. Add {id} to the macro's permissions to allow it."));
141                }
142            }
143            Policy::Deny(why) => err = Some(format!("Step {path} ({id}) can't run in a macro: {why}.")),
144        }
145    });
146    match err {
147        Some(e) => Err(EngineError::Other(format!("Macro '{}' can't run. {e}", m.name))),
148        None => Ok(asks.into_iter().collect()),
149    }
150}
151
152/// Calls `f(path, id, params)` for each enabled command step (skipping disabled steps' bodies).
153fn walk_enabled(steps: &[Step], prefix: &str, f: &mut impl FnMut(&str, &str, &Json)) {
154    for (i, st) in steps.iter().enumerate() {
155        if !st.enabled {
156            continue;
157        }
158        let path = child_path(prefix, i);
159        match &st.kind {
160            StepKind::Command { id, params } => f(&path, id, params),
161            StepKind::Repeat { body, .. } => walk_enabled(body, &path, f),
162            StepKind::If { then, otherwise, .. } => {
163                walk_enabled(then, &format!("{path}.then"), f);
164                walk_enabled(otherwise, &format!("{path}.else"), f);
165            }
166            StepKind::Stop { .. } => {}
167        }
168    }
169}
170
171fn child_path(prefix: &str, i: usize) -> String {
172    if prefix.is_empty() { (i + 1).to_string() } else { format!("{prefix}.{}", i + 1) }
173}
174
175/// Approves the macro for this session when `approve` is set, or asks (an
176/// [`EngineError::Approve`]) when it needs approval it doesn't have.
177pub fn approve(s: &mut Session, m: &Macro, doc: u64, asks: &[String], approve: bool) -> Result<()> {
178    if asks.is_empty() {
179        return Ok(());
180    }
181    let key = (doc, super::fingerprint(m));
182    if s.macro_approvals.contains(&key) {
183        return Ok(());
184    }
185    if approve {
186        s.macro_approvals.insert(key);
187        return Ok(());
188    }
189    Err(EngineError::Approve(format!("Run macro '{}'? It {}.", m.name, join_and(asks))))
190}
191
192fn join_and(items: &[String]) -> String {
193    match items {
194        [] => String::new(),
195        [one] => one.clone(),
196        [init @ .., last] => format!("{} and {last}", init.join(", ")),
197    }
198}
199
200// ---------------------------------------------------------------- inputs
201
202/// A macro ready to run: its inputs bound and checked, its output anchor resolved.
203#[derive(Clone, Debug)]
204pub struct Prepared {
205    pub mac: Macro,
206    pub doc: u64,
207    pub trust: Trust,
208    pub inputs: Vec<Bound>,
209    pub output: Option<Place>,
210}
211
212impl Prepared {
213    pub fn inputs_json(&self, wb: &Workbook) -> Json {
214        let env = Env { wb, active_sheet: wb.active_sheet(), inputs: &[], output: None, item: None };
215        Json::Object(self.inputs.iter().map(|(n, p, _)| (n.clone(), json!(resolve::a1(*p, &env)))).collect())
216    }
217}
218
219/// Binds the macro's inputs on the active document (`given`: `{"Input": "B2:D9"}`, or a reference
220/// for the first input) and checks them. Errors list every problem found.
221pub fn prepare(s: &Session, m: &Macro, given: Option<&Json>) -> Result<Prepared> {
222    let d = s.doc()?;
223    let wb = &d.wb;
224    let mut inputs: Vec<Bound> = Vec::new();
225    let mut problems = Vec::new();
226    for (i, input) in m.inputs.iter().enumerate() {
227        let named = given.and_then(Json::as_object).and_then(|o| o.iter().find(|(k, _)| k.eq_ignore_ascii_case(&input.name)).map(|(_, v)| v));
228        let text = named.and_then(Json::as_str).or_else(|| if i == 0 { given.and_then(Json::as_str) } else { None }).or(input.default_ref.as_deref());
229        let place = match text {
230            Some(t) => {
231                let env = Env { wb, active_sheet: wb.active_sheet(), inputs: &inputs, output: None, item: None };
232                match resolve::resolve_ref(t, &env) {
233                    Ok(p) => p,
234                    Err(e) => {
235                        problems.push(format!("{}: {}", input.name, e.message()));
236                        continue;
237                    }
238                }
239            }
240            None if i == 0 => Place { sheet: wb.active_sheet(), range: d.selection.current() },
241            None => {
242                problems.push(format!("{0} needs a range: pass inputs: {{\"{0}\": \"A1:B5\"}}.", input.name));
243                continue;
244            }
245        };
246        let place = trim(wb, place);
247        problems.extend(check_input(wb, input, place));
248        inputs.push((input.name.clone(), place, input.header));
249    }
250    if !problems.is_empty() {
251        return Err(EngineError::Other(format!("Macro '{}' can't run on these inputs. {}", m.name, problems.join(" "))));
252    }
253    let output = match &m.output.anchor {
254        Some(a) if !a.trim().is_empty() => {
255            let env = Env { wb, active_sheet: wb.active_sheet(), inputs: &inputs, output: None, item: None };
256            Some(
257                resolve::resolve_ref(a, &env)
258                    .map_err(|e| EngineError::Other(format!("Macro '{}': its output anchor {}", m.name, lower_first(e.message()))))?,
259            )
260        }
261        _ => None,
262    };
263    Ok(Prepared { mac: m.clone(), doc: d.uid, trust: d.trust, inputs, output })
264}
265
266fn lower_first(s: &str) -> String {
267    let mut c = s.chars();
268    match c.next() {
269        Some(f) => f.to_lowercase().chain(c).collect(),
270        None => String::new(),
271    }
272}
273
274/// Whole rows or columns shrink to the part of the sheet in use.
275fn trim(wb: &Workbook, p: Place) -> Place {
276    if !(p.range.is_full_cols() || p.range.is_full_rows()) {
277        return p;
278    }
279    let used = wb.sheet(p.sheet).and_then(Sheet::used_range);
280    let range = used.and_then(|u| p.range.intersection(&u)).unwrap_or(RangeRef::cell(p.range.start));
281    Place { sheet: p.sheet, range }
282}
283
284/// What is wrong with `place` as `input`, one sentence each.
285fn check_input(wb: &Workbook, input: &crate::model::NamedInput, place: Place) -> Vec<String> {
286    let mut out = Vec::new();
287    let r = place.range;
288    let name = &input.name;
289    let at = r.a1();
290    if input.kind == InputKind::Cell && !r.is_single() {
291        out.push(format!("{name} must be a single cell; {at} is {} cells.", r.count()));
292        return out;
293    }
294    let header = u32::from(input.header && r.height() > 0);
295    let rows = r.height().saturating_sub(header);
296    let between = |what: &str, n: u32, [min, max]: [u32; 2]| -> Option<String> {
297        let plural = |k: u32| if k == 1 { what.to_string() } else { format!("{what}s") };
298        if max > 0 && min == max && n != min {
299            Some(format!("exactly {min} {}", plural(min)))
300        } else if n < min {
301            Some(format!("at least {min} {}", plural(min)))
302        } else if max > 0 && n > max {
303            Some(format!("at most {max} {}", plural(max)))
304        } else {
305            None
306        }
307    };
308    if let Some(want) = between("data row", rows, input.rows) {
309        out.push(format!("{name} needs {want}; {at} has {rows}."));
310    }
311    if let Some(want) = between("column", r.width(), input.cols) {
312        out.push(format!("{name} needs {want}; {at} has {}.", r.width()));
313    }
314    if r.count() > MAX_INPUT_CELLS {
315        out.push(format!("{name} is too large to check ({} cells; at most {}).", r.count(), MAX_INPUT_CELLS));
316        return out;
317    }
318    let Some(sh) = wb.sheet(place.sheet) else { return out };
319    let mut blanks = Vec::new();
320    for (ci, col) in (r.start.col..=r.end.col).enumerate() {
321        let ty = input.col_types.get(ci).copied().unwrap_or_default();
322        let mut wrong = Vec::new();
323        for row in r.start.row + header..=r.end.row {
324            let c = CellRef::new(row, col);
325            let v = sh.value(c);
326            if v.is_empty() || v.as_text() == Some("") {
327                if input.non_empty {
328                    blanks.push(c);
329                }
330                continue;
331            }
332            let date = || wb.styles.get(sh.style_id(c)).num_fmt.as_str().to_string();
333            let ok = match ty {
334                crate::model::ColType::Any => true,
335                crate::model::ColType::Number => v.is_number(),
336                crate::model::ColType::Text => v.is_text(),
337                crate::model::ColType::Bool => matches!(v, Value::Bool(_)),
338                crate::model::ColType::Date => v.is_number() && crate::engine::display::number_format(&date()).is_date(),
339            };
340            if !ok {
341                wrong.push((c, holds(&v, ty)));
342            }
343        }
344        if !wrong.is_empty() {
345            let head = if input.header {
346                Some(crate::engine::display::cell_text(wb, sh, CellRef::new(r.start.row, col))).filter(|h| !h.trim().is_empty())
347            } else {
348                None
349            };
350            let column = match head {
351                Some(h) => format!("{name} column {} ({})", ci + 1, h.trim()),
352                None => format!("{name} column {}", ci + 1),
353            };
354            let listed: Vec<String> = wrong.iter().take(3).map(|(c, what)| format!("{} holds {what}", c.a1())).collect();
355            let more = if wrong.len() > 3 { format!(" and {} more cells don't", wrong.len() - 3) } else { String::new() };
356            out.push(format!("{column} must hold {}; {}{more}.", ty.noun(), listed.join(", ")));
357        }
358    }
359    if let Some(first) = blanks.first() {
360        let more = if blanks.len() > 1 { format!(" (and {} more)", blanks.len() - 1) } else { String::new() };
361        out.push(format!("{name} must have no blank cells; {} is blank{more}.", first.a1()));
362    }
363    out
364}
365
366/// What a cell holds, for "C7 holds text".
367fn holds(v: &Value, wanted: crate::model::ColType) -> &'static str {
368    match v {
369        Value::Number(_) if wanted == crate::model::ColType::Date => "a number that isn't a date",
370        Value::Number(_) => "a number",
371        Value::Text(_) => "text",
372        Value::Bool(_) => "TRUE or FALSE",
373        Value::Error(_) => "an error",
374        Value::Array(_) => "an array",
375        Value::Empty => "nothing",
376    }
377}
378
379// ---------------------------------------------------------------- the machine
380
381/// A stopwatch for the time limit. `Instant::now` traps on wasm, so there it measures nothing.
382#[derive(Clone, Copy, Debug)]
383pub struct Stopwatch {
384    #[cfg(not(target_arch = "wasm32"))]
385    start: std::time::Instant,
386}
387
388impl Stopwatch {
389    pub fn start() -> Stopwatch {
390        #[cfg(not(target_arch = "wasm32"))]
391        #[allow(clippy::disallowed_methods)] // never on wasm (see above)
392        let w = Stopwatch { start: std::time::Instant::now() };
393        #[cfg(target_arch = "wasm32")]
394        let w = Stopwatch {};
395        w
396    }
397
398    /// Milliseconds since the start, or `None` where there is no clock.
399    fn elapsed_ms(&self) -> Option<f64> {
400        #[cfg(not(target_arch = "wasm32"))]
401        let ms = Some(self.start.elapsed().as_secs_f64() * 1000.0);
402        #[cfg(target_arch = "wasm32")]
403        let ms = None;
404        ms
405    }
406}
407
408#[derive(Clone, Debug)]
409struct Frame {
410    steps: Arc<Vec<Step>>,
411    next: usize,
412    /// Path of the step that opened this frame (`""` at the top, `3` for a Repeat's body,
413    /// `4.then` for an If's branch).
414    prefix: String,
415    repeat: Option<Loop>,
416}
417
418#[derive(Clone, Copy, Debug)]
419struct Loop {
420    of: RepeatOf,
421    over: Place,
422    /// The current item (0-based) and how many there are.
423    at: u32,
424    count: u32,
425}
426
427impl Loop {
428    fn item(&self) -> Place {
429        let r = self.over.range;
430        let range = match self.of {
431            RepeatOf::Rows => {
432                let row = r.start.row.saturating_add(self.at);
433                RangeRef::new(CellRef::new(row, r.start.col), CellRef::new(row, r.end.col))
434            }
435            RepeatOf::Columns => {
436                let col = r.start.col.saturating_add(self.at);
437                RangeRef::new(CellRef::new(r.start.row, col), CellRef::new(r.end.row, col))
438            }
439        };
440        Place { sheet: self.over.sheet, range }
441    }
442}
443
444/// A macro run in progress: which step is next, and what has run.
445#[derive(Clone, Debug)]
446pub struct Machine {
447    pub name: String,
448    pub doc: u64,
449    pub inputs: Vec<Bound>,
450    pub output: Option<Place>,
451    permissions: Vec<String>,
452    trust: Trust,
453    frames: Vec<Frame>,
454    /// Steps executed so far (each pass through a Repeat counts).
455    pub executed: u64,
456    clock: Stopwatch,
457    pub time_limit_ms: f64,
458    /// Dry run (the footprint check): steps that would touch files or can't be previewed stop
459    /// the run early instead of running.
460    pub dry_run: bool,
461    /// A Stop step ended the run: its message, and whether it asked for a rollback.
462    pub stopped: Option<(String, bool)>,
463    /// A dry run stopped before this step (path, reason): what follows isn't in the footprint.
464    pub partial: Option<(String, String)>,
465    /// The last step's resolved parameters (for the debugger).
466    pub last_params: Option<Json>,
467    /// Files the run wrote ("step 1 (file.exportCsv)"): rolling the run back doesn't take them back.
468    pub wrote_files: Vec<String>,
469    /// Repeat and If bodies by the path of their frame: a body is copied the first time its step
470    /// runs, not on every pass.
471    bodies: HashMap<String, Arc<Vec<Step>>>,
472    /// Bytes of parameters passed to commands so far, against [`MAX_PARAM_BYTES`].
473    param_bytes: u64,
474    /// Set unless the macro may overwrite: each step is checked for writes over data as it runs,
475    /// since the footprint can miss a write (it stopped early, or the run took another path).
476    guard: Option<Guard>,
477}
478
479/// Checks each step of a real run for writes over data the footprint didn't see.
480#[derive(Clone, Debug)]
481struct Guard {
482    /// The workbook when the run started, and for each sheet now, its index there (`None` for a
483    /// sheet the run added).
484    start: Arc<Workbook>,
485    origin: Vec<Option<usize>>,
486    /// Where the footprint stopped, if it did: "step 2 (file.exportCsv writes a file)".
487    partial: Option<String>,
488    /// The conflict rule is ask.
489    ask: bool,
490}
491
492impl Guard {
493    fn new(start: Arc<Workbook>, partial: Option<&Json>, ask: bool) -> Guard {
494        let partial = partial.map(|p| {
495            let text = |k: &str| p.get(k).and_then(Json::as_str).unwrap_or_default().to_string();
496            format!("step {} ({})", text("step"), text("reason"))
497        });
498        Guard { origin: (0..start.sheets.len()).map(Some).collect(), start, partial, ask }
499    }
500
501    /// What one step wrote over that held data when the run started: cells outside the inputs,
502    /// sheets and objects it deleted ("B1 on Sheet1; 1 object"), or `""`.
503    fn check(&mut self, prev: &Workbook, after: &Workbook, inputs: &[Bound]) -> String {
504        let (pairs, _) = pair_sheets(prev, after);
505        let mut origin = vec![None; after.sheets.len()];
506        let mut parts = Vec::new();
507        let mut objects = 0usize;
508        for (pi, ai) in pairs {
509            // A sheet the run added holds nothing of the person's.
510            let Some(si) = self.origin.get(pi).copied().flatten() else { continue };
511            let Some(ss) = self.start.sheets.get(si) else { continue };
512            let Some(ai) = ai else {
513                parts.push(format!("the sheet {}", ss.name));
514                continue;
515            };
516            if let Some(o) = origin.get_mut(ai) {
517                *o = Some(si);
518            }
519            let (Some(ps), Some(sa)) = (prev.sheets.get(pi), after.sheets.get(ai)) else { continue };
520            if Arc::ptr_eq(ps, sa) {
521                continue;
522            }
523            // The inputs have followed their cells, so they are on the sheet's index now.
524            let in_input = |c: CellRef| inputs.iter().any(|(_, p, _)| p.sheet == ai && p.range.contains(c));
525            let mut hit: Vec<CellRef> = ps
526                .cells
527                .diff(&sa.cells)
528                .into_iter()
529                .filter(|&c| !in_input(c) && !recalculated_only(ps.cell(c), sa.cell(c)))
530                .filter(|&c| ss.cell(c).is_some_and(has_content) && content_differs(ss.cell(c), sa.cell(c)))
531                .collect();
532            if !hit.is_empty() {
533                let mut listed = ranges(&mut hit);
534                if listed.len() > 3 {
535                    listed.truncate(3);
536                    listed.push("…".into());
537                }
538                parts.push(format!("{} on {}", listed.join(", "), sa.name));
539            }
540            objects += deleted(&ss.charts, &ps.charts, &sa.charts, |o| o.id)
541                + deleted(&ss.images, &ps.images, &sa.images, |o| o.id)
542                + deleted(&ss.shapes, &ps.shapes, &sa.shapes, |o| o.id)
543                + deleted(&ss.controls, &ps.controls, &sa.controls, |o| o.id);
544        }
545        if objects > 0 {
546            parts.push(format!("{objects} object{}", if objects == 1 { "" } else { "s" }));
547        }
548        self.origin = origin;
549        parts.join("; ")
550    }
551}
552
553/// Objects of `prev` that `after` no longer has and that were there at the start.
554fn deleted<T>(start: &[T], prev: &[T], after: &[T], id: impl Fn(&T) -> u32) -> usize {
555    let kept: BTreeSet<u32> = after.iter().map(&id).collect();
556    let had: BTreeSet<u32> = start.iter().map(&id).collect();
557    prev.iter().map(&id).filter(|i| !kept.contains(i) && had.contains(i)).count()
558}
559
560impl Machine {
561    pub fn new(p: &Prepared, dry_run: bool) -> Machine {
562        let mut m = Machine {
563            name: p.mac.name.clone(),
564            doc: p.doc,
565            inputs: p.inputs.clone(),
566            output: p.output,
567            permissions: p.mac.permissions.clone(),
568            trust: p.trust,
569            frames: vec![Frame { steps: Arc::new(p.mac.steps.clone()), next: 0, prefix: String::new(), repeat: None }],
570            executed: 0,
571            clock: Stopwatch::start(),
572            time_limit_ms: DEFAULT_TIME_LIMIT_MS,
573            dry_run,
574            stopped: None,
575            partial: None,
576            last_params: None,
577            wrote_files: Vec::new(),
578            bodies: HashMap::new(),
579            param_bytes: 0,
580            guard: None,
581        };
582        m.settle();
583        m
584    }
585
586    /// Moves to the next enabled step: past disabled steps, finished frames and loop passes.
587    fn settle(&mut self) {
588        while let Some(f) = self.frames.last_mut() {
589            if let Some(st) = f.steps.get(f.next) {
590                if st.enabled {
591                    return;
592                }
593                f.next += 1;
594                continue;
595            }
596            if let Some(l) = f.repeat.as_mut()
597                && l.at.saturating_add(1) < l.count
598            {
599                l.at += 1;
600                f.next = 0;
601                continue;
602            }
603            self.frames.pop();
604        }
605    }
606
607    pub fn is_done(&self) -> bool {
608        self.frames.is_empty()
609    }
610
611    /// Starts the time limit again (the debugger gives each action its own).
612    pub fn restart_clock(&mut self) {
613        self.clock = Stopwatch::start();
614    }
615
616    /// The current step for the debugger: its path, kind, command, parameters as they would run
617    /// now, and the Repeat pass.
618    pub fn current_json(&self, wb: &Workbook) -> Json {
619        let Some((path, st)) = self.current() else { return Json::Null };
620        let env = self.env(wb);
621        let mut out = json!({"step": path, "breakpoint": st.breakpoint});
622        if !st.note.is_empty() {
623            out["note"] = json!(st.note);
624        }
625        match &st.kind {
626            StepKind::Command { id, params } => {
627                out["kind"] = json!("command");
628                out["command"] = json!(id);
629                match resolve::resolve_params(params, &env) {
630                    Ok(p) => out["params"] = p,
631                    Err(e) => out["paramsError"] = json!(e.message()),
632                }
633            }
634            StepKind::Repeat { over, of, .. } => {
635                out["kind"] = json!("repeat");
636                // Whole rows or columns shrink to the part in use, as when the Repeat runs.
637                out["over"] =
638                    json!(resolve::resolve_ref(over, &env).map(|p| resolve::a1(trim(wb, p), &env)).unwrap_or_else(|e| e.message().to_string()));
639                out["of"] = json!(if *of == RepeatOf::Rows { "rows" } else { "columns" });
640            }
641            StepKind::If { cell, .. } => {
642                out["kind"] = json!("if");
643                out["cell"] = json!(resolve::resolve_ref(cell, &env).map(|p| resolve::a1(p, &env)).unwrap_or_else(|e| e.message().to_string()));
644            }
645            StepKind::Stop { message, .. } => {
646                out["kind"] = json!("stop");
647                out["message"] = json!(message);
648            }
649        }
650        if let Some((at, count)) = self.iteration() {
651            out["iteration"] = json!({"at": at, "of": count});
652        }
653        out
654    }
655
656    /// The step that runs next, with its path.
657    pub fn current(&self) -> Option<(String, &Step)> {
658        let f = self.frames.last()?;
659        f.steps.get(f.next).map(|st| (child_path(&f.prefix, f.next), st))
660    }
661
662    /// The innermost Repeat's pass (1-based) and count.
663    pub fn iteration(&self) -> Option<(u32, u32)> {
664        self.frames.iter().rev().find_map(|f| f.repeat).map(|l| (l.at + 1, l.count))
665    }
666
667    fn item(&self) -> Option<(RepeatOf, Place)> {
668        self.frames.iter().rev().find_map(|f| f.repeat).map(|l| (l.of, l.item()))
669    }
670
671    /// Runs every remaining step.
672    pub fn run_to_end(&mut self, s: &mut Session) -> std::result::Result<(), StepError> {
673        while !self.is_done() {
674            self.step(s)?;
675        }
676        Ok(())
677    }
678
679    /// Runs the current step and moves to the next one.
680    pub fn step(&mut self, s: &mut Session) -> std::result::Result<(), StepError> {
681        // The frame's steps are shared, so the step (and its bodies) isn't copied to run it.
682        let Some((path, steps, i)) = self.frames.last().map(|f| (child_path(&f.prefix, f.next), Arc::clone(&f.steps), f.next)) else { return Ok(()) };
683        let Some(st) = steps.get(i) else { return Ok(()) };
684        if let Some(f) = self.frames.last_mut() {
685            f.next += 1;
686        }
687        let mut r = self.exec(s, &path, st);
688        if let Err(e) = r.as_mut() {
689            e.wrote_files.clone_from(&self.wrote_files);
690            self.frames.clear();
691        }
692        self.settle();
693        r
694    }
695
696    fn exec(&mut self, s: &mut Session, path: &str, st: &Step) -> std::result::Result<(), StepError> {
697        self.executed += 1;
698        let at = |msg: String| StepError::new(path, None, msg);
699        if self.executed > MAX_EXECUTED_STEPS {
700            return Err(at(format!("The macro ran more than {MAX_EXECUTED_STEPS} steps and was stopped.")));
701        }
702        if self.clock.elapsed_ms().is_some_and(|ms| ms > self.time_limit_ms) {
703            return Err(at(format!("The macro ran longer than {} seconds and was stopped.", (self.time_limit_ms / 1000.0).round())));
704        }
705        // Steps run on the macro's workbook, whichever one is in front.
706        let Some(i) = s.docs.iter().position(|d| d.uid == self.doc) else { return Err(at("The workbook was closed.".into())) };
707        s.set_active(i);
708        match &st.kind {
709            StepKind::Command { id, params } => self.command(s, path, id, params),
710            StepKind::Repeat { over, of, max, body } => {
711                let place = {
712                    let wb = &s.doc().map_err(|e| at(e.to_string()))?.wb;
713                    let env = self.env(wb);
714                    let p = resolve::resolve_ref(over, &env).map_err(|e| StepError::resolve(path, None, e))?;
715                    trim(wb, p)
716                };
717                let items = match of {
718                    RepeatOf::Rows => place.range.height(),
719                    RepeatOf::Columns => place.range.width(),
720                };
721                let cap = (*max).min(MAX_EXECUTED_STEPS as u32);
722                if items > cap {
723                    let what = if *of == RepeatOf::Rows { "rows" } else { "columns" };
724                    return Err(at(format!("Repeat over {} has {items} {what}, more than its limit of {cap}.", place.range.a1())));
725                }
726                if items > 0 && body.iter().any(|b| b.enabled) {
727                    let repeat = Some(Loop { of: *of, over: place, at: 0, count: items });
728                    let steps = self.body(path.to_string(), body);
729                    self.frames.push(Frame { steps, next: 0, prefix: path.to_string(), repeat });
730                }
731                Ok(())
732            }
733            StepKind::If { cell, op, value, then, otherwise } => {
734                let v = {
735                    let wb = &s.doc().map_err(|e| at(e.to_string()))?.wb;
736                    let env = self.env(wb);
737                    let p = resolve::resolve_ref(cell, &env).map_err(|e| StepError::resolve(path, None, e))?;
738                    if !p.range.is_single() {
739                        return Err(at(format!("If looks at one cell; {} is {} cells.", p.range.a1(), p.range.count())));
740                    }
741                    wb.sheet(p.sheet).map(|sh| sh.value(p.range.start)).unwrap_or_default()
742                };
743                let (branch, name) = if compare(&v, *op, value) { (then, "then") } else { (otherwise, "else") };
744                if branch.iter().any(|b| b.enabled) {
745                    let prefix = format!("{path}.{name}");
746                    let steps = self.body(prefix.clone(), branch);
747                    self.frames.push(Frame { steps, next: 0, prefix, repeat: None });
748                }
749                Ok(())
750            }
751            StepKind::Stop { message, rollback } => {
752                self.stopped = Some((message.clone(), *rollback));
753                self.frames.clear();
754                Ok(())
755            }
756        }
757    }
758
759    /// After a step that inserted, deleted or moved sheets, rows, columns or cells, moves the
760    /// inputs, the output and the ranges of the Repeats in progress with their cells, as the
761    /// references in formulas move. A step that deletes one of them, or changes the size of a
762    /// range a Repeat is going over, stops the run.
763    fn follow(&mut self, before: &Workbook, after: &Workbook, edits: &[(String, Edit)]) -> std::result::Result<(), String> {
764        let same_sheets = before.sheets.len() == after.sheets.len() && before.sheets.iter().zip(&after.sheets).all(|(b, a)| b.name == a.name);
765        if same_sheets && edits.is_empty() {
766            return Ok(());
767        }
768        let (pairs, _) = pair_sheets(before, after);
769        let follow = |what: &str, p: Place| follow_place(before, after, &pairs, edits, p).ok_or_else(|| format!("It deleted the cells of {what}."));
770        for (name, place, _) in self.inputs.iter_mut() {
771            *place = follow(name, *place)?;
772        }
773        if let Some(o) = self.output {
774            self.output = Some(follow("the output anchor", o)?);
775        }
776        for f in self.frames.iter_mut() {
777            let Some(l) = f.repeat.as_mut() else { continue };
778            let over = follow(&format!("the range Repeat {} goes over", f.prefix), l.over)?;
779            if (over.range.height(), over.range.width()) != (l.over.range.height(), l.over.range.width()) {
780                return Err(format!("It changed the size of the range Repeat {} goes over.", f.prefix));
781            }
782            l.over = over;
783        }
784        Ok(())
785    }
786
787    /// The shared copy of the body that runs under `prefix`.
788    fn body(&mut self, prefix: String, body: &[Step]) -> Arc<Vec<Step>> {
789        Arc::clone(self.bodies.entry(prefix).or_insert_with(|| Arc::new(body.to_vec())))
790    }
791
792    fn env<'a>(&'a self, wb: &'a Workbook) -> Env<'a> {
793        Env { wb, active_sheet: wb.active_sheet(), inputs: &self.inputs, output: self.output, item: self.item() }
794    }
795
796    fn command(&mut self, s: &mut Session, path: &str, id: &str, params: &Json) -> std::result::Result<(), StepError> {
797        let fail = |msg: String| StepError::new(path, Some(id), msg);
798        // The policy sees the parameters the command will get, after tokens became A1 text.
799        let resolved = {
800            let wb = &s.doc().map_err(|e| fail(e.to_string()))?.wb;
801            resolve::resolve_params(params, &self.env(wb)).map_err(|e| StepError::resolve(path, Some(id), e))?
802        };
803        match (macro_policy(id, params, self.trust), macro_policy(id, &resolved, self.trust)) {
804            (_, Policy::Deny(why)) | (Policy::Deny(why), _) => return Err(fail(format!("{id} can't run in a macro: {why}."))),
805            (written, Policy::NeedsPermission(what)) => {
806                if !self.permissions.iter().any(|p| p == id) {
807                    return Err(fail(format!("{id} {what}. Add it to the macro's permissions to allow it.")));
808                }
809                // The gate asked the person about this step only if it saw the file access.
810                if !matches!(written, Policy::NeedsPermission(_)) {
811                    return Err(fail(format!("{id} {what} only once its parameters are filled in, so it wasn't approved.")));
812                }
813                if self.dry_run {
814                    self.partial = Some((path.to_string(), format!("{id} {what}")));
815                    self.frames.clear();
816                    return Ok(());
817                }
818            }
819            (_, Policy::Allow | Policy::Ask(_)) => {}
820        }
821        if self.dry_run && !previewable(id) {
822            self.partial = Some((path.to_string(), format!("{id} can't be previewed")));
823            self.frames.clear();
824            return Ok(());
825        }
826        self.param_bytes = self.param_bytes.saturating_add(json_bytes(&resolved));
827        if self.param_bytes > MAX_PARAM_BYTES {
828            return Err(fail(format!(
829                "The macro passed more than {} MB of parameters to its steps and was stopped.",
830                MAX_PARAM_BYTES / (1024 * 1024)
831            )));
832        }
833        let file_step = matches!(macro_policy(id, &resolved, self.trust), Policy::NeedsPermission(_));
834        let writes = writes_file(id, &resolved);
835        self.last_params = Some(resolved.clone());
836        let prev = s.docs.iter_mut().find(|d| d.uid == self.doc).map(|d| {
837            d.edits = Some(Vec::new());
838            d.wb.clone()
839        });
840        let queued = s.ui_requests.len();
841        // Only an approved file step reaches the file system: any other step runs as a script's
842        // does, so a command that reads or writes a file under another parameter name can't.
843        let sandbox = (!file_step).then(crate::engine::io::ScriptSandbox::enter);
844        let r = s.execute(id, resolved);
845        drop(sandbox);
846        let edits = s.docs.iter_mut().find(|d| d.uid == self.doc).and_then(|d| d.edits.take()).unwrap_or_default();
847        // Macros never open dialogs or web pages, and never start editing a cell; messages for
848        // the person stay queued.
849        let mut blocked = None;
850        let asked: Vec<UiRequest> = s.ui_requests.drain(queued.min(s.ui_requests.len())..).collect();
851        for q in asked {
852            match q {
853                UiRequest::Dialog(name, _) => blocked = Some(format!("{id} would open the {name} dialog; give the step the parameters it needs.")),
854                UiRequest::OpenUrl(_) => blocked = Some(format!("{id} would open a web page, which macros don't do.")),
855                UiRequest::EditCell(_) | UiRequest::InsertName(_) => {}
856                m @ UiRequest::Message(_) => s.ui_requests.push(m),
857            }
858        }
859        r.map_err(|e| fail(e.to_string()))?;
860        if writes {
861            self.wrote_files.push(format!("step {path} ({id})"));
862        }
863        if let Some(b) = blocked {
864            return Err(fail(b));
865        }
866        let after = s.docs.iter().find(|d| d.uid == self.doc).map(|d| d.wb.clone()).ok_or_else(|| fail("The workbook was closed.".into()))?;
867        if let Some(prev) = &prev {
868            self.follow(prev, &after, &edits).map_err(fail)?;
869        }
870        if let (Some(g), Some(prev)) = (self.guard.as_mut(), prev) {
871            let what = g.check(&prev, &after, &self.inputs);
872            if !what.is_empty() {
873                let msg = match &g.partial {
874                    Some(at) => format!("It would write over data ({what}); the check before the run stopped at {at} and couldn't see this."),
875                    // A value such as RAND or NOW differed from the check's, and the run went another way.
876                    None => format!("It would write over data ({what}); the check before the run didn't see this write."),
877                };
878                return Err(StepError { confirm: g.ask, ..fail(msg) });
879            }
880        }
881        Ok(())
882    }
883}
884
885/// Where the cells of `p` are after a step: on the same sheet wherever it moved (`pairs`, from
886/// [`pair_sheets`]), and moved by each structural edit the step made there, or `None` when the
887/// step deleted them (or their sheet).
888fn follow_place(before: &Workbook, after: &Workbook, pairs: &[(usize, Option<usize>)], edits: &[(String, Edit)], p: Place) -> Option<Place> {
889    let sheet = pairs.iter().find(|(b, _)| *b == p.sheet).and_then(|(_, a)| *a)?;
890    let mut name = after.sheet(sheet)?.name.clone();
891    let old_name = before.sheet(p.sheet).map(|s| s.name.as_str()).unwrap_or_default();
892    let mut text = format!("{}!{}", resolve::quote_sheet(&name), p.range.a1());
893    for (target, e) in edits {
894        if target != &name && target != old_name {
895            continue;
896        }
897        let expr = crate::formula::parse(&text).ok()?;
898        text = crate::formula::print(&crate::formula::adjust::adjust(expr, target, target, e));
899        if text.contains("#REF!") {
900            return None;
901        }
902        // A cut and paste can take the cells to another sheet.
903        let env = Env { wb: after, active_sheet: usize::MAX, inputs: &[], output: None, item: None };
904        name = after.sheet(resolve::resolve_ref(&text, &env).ok()?.sheet)?.name.clone();
905    }
906    let env = Env { wb: after, active_sheet: usize::MAX, inputs: &[], output: None, item: None };
907    if text.contains('$') {
908        text = text.replace('$', "");
909    }
910    resolve::resolve_ref(&text, &env).ok()
911}
912
913/// If's comparison of a cell's value with a step's `value`, the way Excel's `=`, `<`… compare.
914fn compare(v: &Value, op: CompareOp, want: &Json) -> bool {
915    let empty = v.is_empty() || v.as_text() == Some("");
916    let want_text = || match want {
917        Json::String(t) => t.clone(),
918        Json::Null => String::new(),
919        other => other.to_string(),
920    };
921    let ord = || -> Option<std::cmp::Ordering> {
922        let w = match want {
923            Json::Null => Value::Empty,
924            Json::Bool(b) => Value::Bool(*b),
925            Json::Number(n) => Value::number(n.as_f64()?),
926            // "5" compares as a number with a number in the cell.
927            Json::String(t) => match (v, t.trim().parse::<f64>()) {
928                (Value::Number(_), Ok(n)) => Value::number(n),
929                _ => Value::text(t.as_str()),
930            },
931            _ => return None,
932        };
933        match (v, &w) {
934            (Value::Error(e), Value::Text(t)) => {
935                Some(if e.as_str().eq_ignore_ascii_case(t.trim()) { std::cmp::Ordering::Equal } else { std::cmp::Ordering::Less })
936            }
937            (Value::Error(_), _) | (_, Value::Error(_)) | (Value::Array(_), _) => None,
938            _ => Some(crate::base::value::compare(v, &w)),
939        }
940    };
941    use std::cmp::Ordering::*;
942    match op {
943        CompareOp::Empty => empty,
944        CompareOp::NotEmpty => !empty,
945        CompareOp::Contains => v.display().to_lowercase().contains(&want_text().to_lowercase()),
946        CompareOp::Eq => ord() == Some(Equal),
947        CompareOp::Ne => ord() != Some(Equal),
948        CompareOp::Gt => ord() == Some(Greater),
949        CompareOp::Ge => matches!(ord(), Some(Greater | Equal)),
950        CompareOp::Lt => ord() == Some(Less),
951        CompareOp::Le => matches!(ord(), Some(Less | Equal)),
952    }
953}
954
955// ---------------------------------------------------------------- footprint
956
957/// What a macro would change, from a dry run on the real workbook.
958#[derive(Clone, Debug, Default, Serialize)]
959#[serde(rename_all = "camelCase")]
960pub struct Footprint {
961    /// Steps executed and bytes of parameters passed by the end of the dry run that made this
962    /// footprint (the run's one budget).
963    #[serde(skip)]
964    pub executed: u64,
965    #[serde(skip)]
966    pub param_bytes: u64,
967    /// The Stop step that ended the dry run asks for the run to be rolled back.
968    #[serde(skip)]
969    pub rolls_back: bool,
970    /// The dry run wrote more cells than the touch log keeps, so writes that kept a cell's value
971    /// may be missing from `cells`: a shift cannot trust the block.
972    #[serde(skip_serializing_if = "std::ops::Not::not")]
973    pub log_full: bool,
974    pub sheets: Vec<SheetFootprint>,
975    pub sheets_added: Vec<String>,
976    pub sheets_removed: Vec<String>,
977    pub objects_added: usize,
978    pub objects_changed: usize,
979    pub objects_removed: usize,
980    /// The dry run stopped early before this step; later steps aren't in the footprint.
981    #[serde(skip_serializing_if = "Option::is_none")]
982    pub partial: Option<Json>,
983    /// The step the run would fail at.
984    #[serde(skip_serializing_if = "Option::is_none")]
985    pub error: Option<StepError>,
986    /// A Stop step's message.
987    #[serde(skip_serializing_if = "Option::is_none")]
988    pub stopped: Option<String>,
989}
990
991/// The cells a macro would write on one sheet, outside its inputs.
992#[derive(Clone, Debug, Default, Serialize)]
993#[serde(rename_all = "camelCase")]
994pub struct SheetFootprint {
995    pub sheet: String,
996    #[serde(skip)]
997    pub index: usize,
998    /// The written cells as ranges (at most 500 listed), and how many cells.
999    pub cells: Vec<String>,
1000    pub changed: usize,
1001    /// Written cells that hold data now.
1002    pub conflicts: Vec<String>,
1003    pub conflict_count: usize,
1004    /// The block that holds every written cell.
1005    #[serde(skip_serializing_if = "Option::is_none")]
1006    pub bounds: Option<String>,
1007    #[serde(skip)]
1008    pub bounds_range: Option<RangeRef>,
1009}
1010
1011impl Footprint {
1012    /// Cells that hold data and would be written, plus sheets and objects that would go.
1013    pub fn conflict_count(&self) -> usize {
1014        self.sheets.iter().map(|s| s.conflict_count).sum::<usize>() + self.sheets_removed.len() + self.objects_removed
1015    }
1016
1017    /// "C2:C9 on Sheet1, the sheet Old, 2 objects".
1018    pub fn describe_conflicts(&self) -> String {
1019        let mut parts: Vec<String> = self
1020            .sheets
1021            .iter()
1022            .filter(|s| s.conflict_count > 0)
1023            .map(|s| {
1024                let mut listed = s.conflicts.iter().take(3).cloned().collect::<Vec<_>>().join(", ");
1025                if s.conflicts.len() > 3 {
1026                    listed.push_str(", …");
1027                }
1028                format!("{listed} on {}", s.sheet)
1029            })
1030            .collect();
1031        parts.extend(self.sheets_removed.iter().map(|n| format!("the sheet {n}")));
1032        if self.objects_removed > 0 {
1033            parts.push(format!("{} object{}", self.objects_removed, if self.objects_removed == 1 { "" } else { "s" }));
1034        }
1035        parts.join("; ")
1036    }
1037}
1038
1039/// Session state a dry run may touch, put back afterwards. The workbook, its selection and its
1040/// history are put back by the dry run's undo group.
1041struct Saved {
1042    active: usize,
1043    docs: Vec<u64>,
1044    /// The workbook as last saved: `sheet.activate` and the view commands move it to the
1045    /// current workbook on a clean document, and the rollback puts the workbook back but not this.
1046    saved: Option<Arc<Workbook>>,
1047    /// The document's revision: the rollback counts as a change, and front ends key their caches
1048    /// on it, so a check that changed nothing leaves it as it was.
1049    revision: Option<u64>,
1050    sheet_selections: Vec<Selection>,
1051    unlocked_ranges: Vec<(String, String)>,
1052    trust: Trust,
1053    go_to_recent: Vec<String>,
1054    clipboard: Option<crate::engine::Clipboard>,
1055    prefs: crate::engine::Prefs,
1056    mode: crate::engine::Mode,
1057    /// The journal's length: the dry run journals nothing (`Session::journal_paused`), so nothing
1058    /// of the person's is evicted, and the length puts back exactly what was there.
1059    journal: usize,
1060    recording: Option<usize>,
1061    ui_requests: usize,
1062    format_painter: Option<(Arc<Workbook>, usize, RangeRef, bool)>,
1063    last_find: Option<Json>,
1064    hide_ruler: bool,
1065    windows: (Vec<crate::engine::WindowState>, Option<u64>, u64, u64),
1066    draw: (String, String, f32),
1067    comments_pane: bool,
1068    show_all_notes: bool,
1069}
1070
1071impl Saved {
1072    /// Takes the state and stops journaling until [`Saved::restore`].
1073    fn take(s: &mut Session, doc: u64) -> Saved {
1074        let d = s.docs.iter().find(|d| d.uid == doc);
1075        let saved = Saved {
1076            active: s.active,
1077            docs: s.docs.iter().map(|d| d.uid).collect(),
1078            saved: d.map(|d| d.saved.clone()),
1079            revision: d.map(|d| d.revision),
1080            sheet_selections: d.map(|d| d.sheet_selections.clone()).unwrap_or_default(),
1081            unlocked_ranges: d.map(|d| d.unlocked_ranges.clone()).unwrap_or_default(),
1082            trust: d.map_or(Trust::Local, |d| d.trust),
1083            go_to_recent: d.map(|d| d.go_to_recent.clone()).unwrap_or_default(),
1084            clipboard: s.clipboard.clone(),
1085            prefs: s.prefs.clone(),
1086            mode: s.mode,
1087            journal: s.journal.len(),
1088            recording: s.recording,
1089            ui_requests: s.ui_requests.len(),
1090            format_painter: s.format_painter.clone(),
1091            last_find: s.last_find.clone(),
1092            hide_ruler: s.hide_ruler,
1093            windows: (s.windows.clone(), s.active_window, s.next_window, s.activation),
1094            draw: (s.draw_tool.clone(), s.draw_color.clone(), s.draw_width),
1095            comments_pane: s.comments_pane,
1096            show_all_notes: s.show_all_notes,
1097        };
1098        s.journal_paused = s.journal_paused.saturating_add(1);
1099        saved
1100    }
1101
1102    fn restore(self, s: &mut Session, doc: u64) {
1103        s.docs.retain(|d| self.docs.contains(&d.uid));
1104        if let Some(d) = s.docs.iter_mut().find(|d| d.uid == doc) {
1105            if let Some(saved) = self.saved {
1106                d.saved = saved;
1107            }
1108            if let Some(revision) = self.revision {
1109                d.revision = revision;
1110            }
1111            d.sheet_selections = self.sheet_selections;
1112            d.unlocked_ranges = self.unlocked_ranges;
1113            d.trust = self.trust;
1114            d.go_to_recent = self.go_to_recent;
1115        }
1116        s.active = self.active.min(s.docs.len().saturating_sub(1));
1117        s.clipboard = self.clipboard;
1118        s.prefs = self.prefs;
1119        s.mode = self.mode;
1120        s.truncate_journal(self.journal);
1121        s.recording = self.recording;
1122        s.journal_paused = s.journal_paused.saturating_sub(1);
1123        s.ui_requests.truncate(self.ui_requests);
1124        s.format_painter = self.format_painter;
1125        s.last_find = self.last_find;
1126        s.hide_ruler = self.hide_ruler;
1127        (s.windows, s.active_window, s.next_window, s.activation) = self.windows;
1128        (s.draw_tool, s.draw_color, s.draw_width) = self.draw;
1129        s.comments_pane = self.comments_pane;
1130        s.show_all_notes = self.show_all_notes;
1131    }
1132}
1133
1134/// Runs the macro in dry-run mode on its workbook, puts everything back, and says what it would
1135/// change. The dry run spends from `run`'s budget: its clock and time limit, its steps and the
1136/// bytes of parameters it passed.
1137pub fn footprint(s: &mut Session, p: &Prepared, run: &Machine) -> Result<Footprint> {
1138    let i = s.docs.iter().position(|d| d.uid == p.doc).ok_or(EngineError::NoDocument)?;
1139    s.set_active(i);
1140    let before = s.doc()?.wb.clone();
1141    let token = s.group_begin("Check Macro")?;
1142    // After the calls that can fail, so nothing returns between taking and putting back.
1143    let saved = Saved::take(s, p.doc);
1144    if let Some(d) = s.docs.iter_mut().find(|d| d.uid == p.doc) {
1145        d.touched = Some(Vec::new());
1146    }
1147    let mut m = Machine::new(p, true);
1148    (m.clock, m.time_limit_ms, m.executed, m.param_bytes) = (run.clock, run.time_limit_ms, run.executed, run.param_bytes);
1149    let outcome = m.run_to_end(s);
1150    let (after, touched) = match s.docs.iter_mut().find(|d| d.uid == p.doc) {
1151        Some(d) => (d.wb.clone(), d.touched.take().unwrap_or_default()),
1152        None => (before.clone(), Vec::new()),
1153    };
1154    let rolled = s.group_rollback(token);
1155    saved.restore(s, p.doc);
1156    rolled?;
1157    let (after, touched) = if matches!(m.stopped, Some((_, true))) { (before.clone(), Vec::new()) } else { (after, touched) };
1158    let mut fp = diff(&before, &after, &p.inputs, &touched);
1159    fp.log_full = touched.len() >= crate::engine::cmd::TOUCH_LOG_LIMIT;
1160    (fp.executed, fp.param_bytes) = (m.executed, m.param_bytes);
1161    fp.rolls_back = matches!(m.stopped, Some((_, true)));
1162    fp.error = outcome.err();
1163    fp.stopped = m.stopped.map(|(msg, _)| msg);
1164    fp.partial = m.partial.map(|(step, reason)| json!({"step": step, "reason": reason}));
1165    Ok(fp)
1166}
1167
1168/// Compares the workbook before and after a dry run. `touched` adds the cells commands wrote
1169/// without changing them (a formula written again), so a shift moves everything the macro writes.
1170fn diff(before: &Workbook, after: &Workbook, inputs: &[Bound], touched: &[(String, CellRef)]) -> Footprint {
1171    let mut fp = Footprint::default();
1172    // One pass over the touch log (up to a million writes), not one per sheet.
1173    let mut by_sheet: HashMap<&str, BTreeSet<CellRef>> = HashMap::new();
1174    for (n, c) in touched {
1175        by_sheet.entry(n.as_str()).or_default().insert(*c);
1176    }
1177    let (pairs, added) = pair_sheets(before, after);
1178    fp.sheets_added = added.into_iter().filter_map(|a| after.sheet(a).map(|s| s.name.clone())).collect();
1179    for (bi, ai) in pairs {
1180        let Some(bs) = before.sheets.get(bi) else { continue };
1181        let Some(ai) = ai else {
1182            fp.sheets_removed.push(bs.name.clone());
1183            continue;
1184        };
1185        let Some(after_sheet) = after.sheets.get(ai) else { continue };
1186        let rewritten = by_sheet.remove(after_sheet.name.as_str()).unwrap_or_default();
1187        if Arc::ptr_eq(bs, after_sheet) && rewritten.is_empty() {
1188            continue;
1189        }
1190        let (added, changed, removed) = objects(bs, after_sheet);
1191        fp.objects_added += added;
1192        fp.objects_changed += changed;
1193        fp.objects_removed += removed;
1194        let in_input = |c: CellRef| inputs.iter().any(|(_, p, _)| p.sheet == bi && p.range.contains(c));
1195        let mut written = Vec::new();
1196        let mut conflicts = Vec::new();
1197        let mut cells: BTreeSet<CellRef> = bs.cells.diff(&after_sheet.cells).into_iter().collect();
1198        cells.extend(rewritten.iter().copied());
1199        for c in cells {
1200            if in_input(c) {
1201                continue;
1202            }
1203            let (b, a) = (bs.cell(c), after_sheet.cell(c));
1204            if recalculated_only(b, a) && !rewritten.contains(&c) {
1205                continue;
1206            }
1207            written.push(c);
1208            if b.is_some_and(has_content) && content_differs(b, a) {
1209                conflicts.push(c);
1210            }
1211        }
1212        let Some(first) = written.first() else { continue };
1213        let bounds = written.iter().fold(RangeRef::cell(*first), |r, c| r.union(&RangeRef::cell(*c)));
1214        fp.sheets.push(SheetFootprint {
1215            sheet: bs.name.clone(),
1216            index: bi,
1217            changed: written.len(),
1218            conflict_count: conflicts.len(),
1219            cells: ranges(&mut written),
1220            conflicts: ranges(&mut conflicts),
1221            bounds: Some(bounds.a1()),
1222            bounds_range: Some(bounds),
1223        });
1224    }
1225    fp
1226}
1227
1228/// Each sheet of `before` with its index in `after` (`None` when deleted), and the sheets of
1229/// `after` that are new. Sheets are matched by name; what's left is matched in order (a renamed
1230/// sheet).
1231pub(crate) fn pair_sheets(before: &Workbook, after: &Workbook) -> (Vec<(usize, Option<usize>)>, Vec<usize>) {
1232    let mut pairs: Vec<(usize, Option<usize>)> = before.sheets.iter().enumerate().map(|(bi, sh)| (bi, after.sheet_index(&sh.name))).collect();
1233    let matched: BTreeSet<usize> = pairs.iter().filter_map(|(_, a)| *a).collect();
1234    let mut spare = (0..after.sheets.len()).filter(|a| !matched.contains(a));
1235    for (_, a) in pairs.iter_mut().filter(|(_, a)| a.is_none()) {
1236        *a = spare.next();
1237    }
1238    (pairs, spare.collect())
1239}
1240
1241/// Only a formula's (or a Python cell's) cached value changed (a recalculation, not a write): a
1242/// Python cell whose spill a step blocked shows #SPILL! with its code untouched.
1243fn recalculated_only(b: Option<&Cell>, a: Option<&Cell>) -> bool {
1244    match (b, a) {
1245        (Some(b), Some(a)) => match (&b.formula, &a.formula) {
1246            (Some(x), Some(y)) => x.text == y.text && x.array == y.array && b.style == a.style,
1247            (None, None) => match (&b.py, &a.py) {
1248                (Some(x), Some(y)) => x.code == y.code && x.object == y.object && b.style == a.style,
1249                _ => false,
1250            },
1251            _ => false,
1252        },
1253        _ => false,
1254    }
1255}
1256
1257/// Python code is content, as a formula is, whatever the cell shows.
1258fn has_content(c: &Cell) -> bool {
1259    c.formula.is_some() || c.py.is_some() || !(c.value.is_empty() || c.value.as_text() == Some(""))
1260}
1261
1262/// Whether a write changed what the person entered: the formula, the Python code and its return
1263/// type, or the value. A Python cell replaced by its own cached value, or by other code that
1264/// shows the same `#BLOCKED!`, has changed.
1265fn content_differs(b: Option<&Cell>, a: Option<&Cell>) -> bool {
1266    match (b, a) {
1267        (Some(b), Some(a)) => match (&b.formula, &a.formula) {
1268            (Some(x), Some(y)) => x.text != y.text,
1269            (None, None) => match (&b.py, &a.py) {
1270                (Some(x), Some(y)) => x.code != y.code || x.object != y.object,
1271                (None, None) => b.value != a.value,
1272                _ => true,
1273            },
1274            _ => true,
1275        },
1276        (Some(b), None) => has_content(b),
1277        _ => false,
1278    }
1279}
1280
1281/// Objects added, changed and removed between two versions of a sheet.
1282fn objects(b: &Sheet, a: &Sheet) -> (usize, usize, usize) {
1283    fn count<T: PartialEq>(b: &[T], a: &[T], id: impl Fn(&T) -> u32) -> (usize, usize, usize) {
1284        let added = a.iter().filter(|x| !b.iter().any(|y| id(y) == id(x))).count();
1285        let removed = b.iter().filter(|x| !a.iter().any(|y| id(y) == id(x))).count();
1286        let changed = b.iter().filter(|x| a.iter().any(|y| id(y) == id(x) && y != *x)).count();
1287        (added, changed, removed)
1288    }
1289    let parts = [
1290        count(&b.charts, &a.charts, |o| o.id),
1291        count(&b.images, &a.images, |o| o.id),
1292        count(&b.shapes, &a.shapes, |o| o.id),
1293        count(&b.controls, &a.controls, |o| o.id),
1294    ];
1295    parts.iter().fold((0, 0, 0), |t, p| (t.0 + p.0, t.1 + p.1, t.2 + p.2))
1296}
1297
1298/// Cells as A1 ranges: runs along each row, then runs with the same columns on following rows.
1299pub(crate) fn ranges(cells: &mut [CellRef]) -> Vec<String> {
1300    cells.sort_unstable_by_key(|c| (c.row, c.col));
1301    let mut runs: Vec<RangeRef> = Vec::new();
1302    for c in cells.iter() {
1303        match runs.last_mut() {
1304            Some(r) if r.end.row == c.row && r.end.col.checked_add(1) == Some(c.col) => r.end.col = c.col,
1305            _ => runs.push(RangeRef::cell(*c)),
1306        }
1307    }
1308    let mut merged: Vec<RangeRef> = Vec::new();
1309    for r in runs {
1310        // The block above with the same columns that ends on the row before.
1311        match merged
1312            .iter_mut()
1313            .rev()
1314            .take(64)
1315            .find(|m| m.start.col == r.start.col && m.end.col == r.end.col && m.end.row.checked_add(1) == Some(r.start.row))
1316        {
1317            Some(m) => m.end.row = r.end.row,
1318            None => merged.push(r),
1319        }
1320    }
1321    merged.iter().take(MAX_LISTED_RANGES).map(RangeRef::a1).collect()
1322}
1323
1324/// Inserts blank cells where the footprint lands (moving what is there down or right), so the
1325/// macro writes into empty cells. The block is every written cell's, so a row the macro writes
1326/// stays together. A shift that would move one of the macro's inputs is refused: the inputs were
1327/// checked and bound where they are, and only the block's columns (or rows) would move.
1328fn make_room(s: &mut Session, fp: &Footprint, p: &Prepared, down: bool) -> Result<()> {
1329    let wb = s.doc()?.wb.clone();
1330    if let Some(why) = shift_refusal(&wb, fp, p, down) {
1331        return Err(EngineError::Other(why));
1332    }
1333    let env = Env { wb: &wb, active_sheet: usize::MAX, inputs: &[], output: None, item: None };
1334    let moves: Vec<String> = fp
1335        .sheets
1336        .iter()
1337        .filter(|s| s.conflict_count > 0)
1338        .filter_map(|sh| sh.bounds_range.map(|block| resolve::a1(Place { sheet: sh.index, range: block }, &env)))
1339        .collect();
1340    for range in moves {
1341        s.execute("home.insertCells", json!({"range": range, "shift": if down { "down" } else { "right" }}))?;
1342    }
1343    Ok(())
1344}
1345
1346/// Why shifting the data can't make room for `fp`, or `None`: the macro deletes sheets or
1347/// objects, the footprint couldn't follow every write, or the shift would move an input.
1348fn shift_refusal(wb: &Workbook, fp: &Footprint, p: &Prepared, down: bool) -> Option<String> {
1349    if !fp.sheets_removed.is_empty() || fp.objects_removed > 0 {
1350        return Some("Shifting can't make room: the macro deletes sheets or objects.".into());
1351    }
1352    if fp.log_full {
1353        return Some("Shifting can't make room: the macro writes more cells than the footprint can follow.".into());
1354    }
1355    let env = Env { wb, active_sheet: usize::MAX, inputs: &[], output: None, item: None };
1356    for sh in fp.sheets.iter().filter(|s| s.conflict_count > 0) {
1357        let Some(block) = sh.bounds_range else { continue };
1358        // What the insertion moves: the block's columns from its top row down, or its rows from
1359        // its left column rightwards.
1360        let moved = |r: &RangeRef| {
1361            if down {
1362                r.start.col <= block.end.col && r.end.col >= block.start.col && r.end.row >= block.start.row
1363            } else {
1364                r.start.row <= block.end.row && r.end.row >= block.start.row && r.end.col >= block.start.col
1365            }
1366        };
1367        // The output is where the macro writes, so moving what is there is the point; the second
1368        // footprint catches data the shift moved into it.
1369        if let Some((name, pl)) = p.inputs.iter().map(|(n, pl, _)| (n.as_str(), *pl)).find(|(_, pl)| pl.sheet == sh.index && moved(&pl.range)) {
1370            return Some(format!(
1371                "Shifting the data would move {name} ({}). Run with onConflict overwrite or fail, or make room first.",
1372                resolve::a1(pl, &env)
1373            ));
1374        }
1375    }
1376    None
1377}
1378
1379// ---------------------------------------------------------------- run
1380
1381/// A macro that passed its checks and is about to run.
1382pub struct Begun {
1383    pub mac: Macro,
1384    pub prepared: Prepared,
1385    pub rule: OnConflict,
1386    pub time_limit_ms: f64,
1387    pub inputs: Json,
1388}
1389
1390/// The checks before a run, on the active workbook: the macro exists, the policy allows it, the
1391/// person approved it if it needs that, and its inputs are right.
1392pub fn begin(s: &mut Session, name: &str, p: &Json) -> Result<Begun> {
1393    let d = s.doc()?;
1394    let (doc, trust) = (d.uid, d.trust);
1395    let m = super::find(&d.wb, name)
1396        .and_then(|i| d.wb.macros.get(i))
1397        .cloned()
1398        .ok_or_else(|| EngineError::Other(format!("There is no macro called '{name}'.")))?;
1399    check_params(p, &m)?;
1400    let asks = gate(&m, trust)?;
1401    approve(s, &m, doc, &asks, p.get("approve").and_then(Json::as_bool).unwrap_or(false))?;
1402    let m = with_output(m, p);
1403    let prepared = prepare(s, &m, p.get("inputs"))?;
1404    let rule = conflict_rule(p, &m)?;
1405    let time_limit_ms = time_limit(p);
1406    let inputs = prepared.inputs_json(&s.doc()?.wb);
1407    Ok(Begun { mac: m, prepared, rule, time_limit_ms, inputs })
1408}
1409
1410/// The footprint check and the output rule, inside the run's undo group: an error when the macro
1411/// would fail or write over data it may not, after shifting the data if the rule says so. Returns
1412/// the machine for the run and whether it shifted. The dry runs count towards the run's time
1413/// limit.
1414///
1415/// The footprint proves nothing about steps it didn't reach (it stopped early) or a path the run
1416/// takes and the dry run didn't (RAND, NOW), so the machine also checks each step as it runs and
1417/// stops the run at one that writes over data.
1418pub fn make_way(s: &mut Session, b: &Begun) -> Result<(Machine, bool)> {
1419    let (m, rule) = (&b.mac, b.rule);
1420    let mut machine = Machine::new(&b.prepared, false);
1421    machine.time_limit_ms = b.time_limit_ms;
1422    if rule == OnConflict::Overwrite {
1423        return Ok((machine, false));
1424    }
1425    // The dry runs and the run share one budget (steps, parameters) as they share one clock.
1426    let mut fp = footprint(s, &b.prepared, &machine)?;
1427    (machine.executed, machine.param_bytes) = (fp.executed, fp.param_bytes);
1428    if let Some(e) = fp.error.take() {
1429        return Err(e.into_engine(&m.name));
1430    }
1431    let mut shifted = false;
1432    if fp.conflict_count() > 0 {
1433        let what = fp.describe_conflicts();
1434        match rule {
1435            OnConflict::Ask => {
1436                // Overwriting answers for every step, also the ones a partial footprint didn't reach.
1437                let unseen = fp.partial.as_ref().map_or(String::new(), |p| {
1438                    let text = |k: &str| p.get(k).and_then(Json::as_str).unwrap_or_default().to_string();
1439                    format!(
1440                        " The check stopped at step {} ({}), so overwriting also lets the steps after it write over data unchecked.",
1441                        text("step"),
1442                        text("reason")
1443                    )
1444                });
1445                return Err(EngineError::Confirm(format!(
1446                    "Macro '{}' would write over cells that hold data ({what}).{unseen} Overwrite them, or run again with onConflict shiftDown, shiftRight or fail",
1447                    m.name
1448                )));
1449            }
1450            OnConflict::Fail | OnConflict::Overwrite => {
1451                return Err(EngineError::Other(format!("Macro '{}' would write over cells that hold data ({what}). Nothing was changed.", m.name)));
1452            }
1453            OnConflict::ShiftDown | OnConflict::ShiftRight => {
1454                make_room(s, &fp, &b.prepared, rule == OnConflict::ShiftDown)?;
1455                fp = footprint(s, &b.prepared, &machine)?;
1456                (machine.executed, machine.param_bytes) = (fp.executed, fp.param_bytes);
1457                if let Some(e) = fp.error.take() {
1458                    return Err(e.into_engine(&m.name));
1459                }
1460                if fp.conflict_count() > 0 {
1461                    return Err(EngineError::Other(format!(
1462                        "Macro '{}': shifting the data didn't make room ({}). Nothing was changed.",
1463                        m.name,
1464                        fp.describe_conflicts()
1465                    )));
1466                }
1467                shifted = true;
1468            }
1469        }
1470    }
1471    let start = s.docs.iter().find(|d| d.uid == b.prepared.doc).ok_or(EngineError::NoDocument)?.wb.clone();
1472    machine.guard = Some(Guard::new(start, fp.partial.as_ref(), rule == OnConflict::Ask));
1473    Ok((machine, shifted))
1474}
1475
1476/// Runs macro `name` on the active workbook: `{inputs?, onConflict?, overwrite?, approve?,
1477/// timeLimit?}` (see `macro.run`).
1478pub fn run(s: &mut Session, name: &str, p: &Json) -> Result<Json> {
1479    let b = begin(s, name, p)?;
1480    let label = format!("Run Macro: {}", b.mac.name);
1481    s.grouped(&label, |s| {
1482        let (mut machine, shifted) = make_way(s, &b)?;
1483        let name = &b.mac.name;
1484        // `ran` counts the run's own steps; the budget also holds the dry runs'.
1485        let before_run = machine.executed;
1486        machine.run_to_end(s).map_err(|e| e.into_engine(name))?;
1487        match machine.stopped {
1488            Some((msg, true)) => Err(EngineError::Other(format!("Macro '{name}' stopped: {msg} {}", rolled_back(&machine.wrote_files)))),
1489            stopped => {
1490                let mut out = json!({"macro": name, "ran": machine.executed.saturating_sub(before_run), "inputs": b.inputs});
1491                if let Some((msg, _)) = stopped {
1492                    out["stopped"] = json!(msg);
1493                }
1494                if shifted {
1495                    out["shifted"] = json!(b.rule.name());
1496                }
1497                Ok(out)
1498            }
1499        }
1500    })
1501}
1502
1503/// `m` with the run's `output` (a form button that sends the output elsewhere) in place of its own
1504/// output anchor, after the policy and the approval, which go by the macro as it is stored. The
1505/// anchor is resolved and checked by [`prepare`] like the macro's own, and the footprint, the
1506/// conflict rule and the per-step guard all work on the cells it names.
1507fn with_output(mut m: Macro, p: &Json) -> Macro {
1508    if let Some(o) = p.get("output").and_then(Json::as_str).map(str::trim).filter(|o| !o.is_empty()) {
1509        m.output.anchor = Some(o.to_string());
1510    }
1511    m
1512}
1513
1514/// Refuses run parameters that would otherwise be ignored: an input the macro doesn't have, an
1515/// input given as something other than reference text, `onConflict`, `overwrite`, `approve` or
1516/// `timeLimit` of the wrong kind, and an `output` that isn't reference text of at most
1517/// `MAX_MACRO_REF` characters (`null` counts as not given).
1518pub fn check_params(p: &Json, m: &Macro) -> Result<()> {
1519    let refuse = |msg: String| Err(EngineError::Other(format!("Macro '{}': {msg}", m.name)));
1520    let names = || m.inputs.iter().map(|i| i.name.clone()).collect::<Vec<_>>().join(", ");
1521    match p.get("inputs") {
1522        None | Some(Json::Null) => {}
1523        Some(Json::String(_)) if !m.inputs.is_empty() => {}
1524        Some(Json::String(_)) => return refuse("it takes no inputs.".into()),
1525        Some(Json::Object(o)) => {
1526            for (k, v) in o {
1527                if !m.inputs.iter().any(|i| i.name.eq_ignore_ascii_case(k)) {
1528                    return refuse(if m.inputs.is_empty() {
1529                        format!("it has no input called '{k}'; it takes no inputs.")
1530                    } else {
1531                        format!("it has no input called '{k}' (its inputs: {}).", names())
1532                    });
1533                }
1534                if !v.is_string() {
1535                    return refuse(format!("give the input {k} as reference text, such as \"B2:D9\"."));
1536                }
1537            }
1538        }
1539        Some(_) => return refuse("give `inputs` as reference text, such as \"B2:D9\", or as {\"Input\": \"B2:D9\"}.".into()),
1540    }
1541    let given = |k: &str| p.get(k).filter(|v| !v.is_null());
1542    if given("onConflict").is_some_and(|v| !v.is_string()) {
1543        return refuse("`onConflict` must be ask, fail, overwrite, shiftDown or shiftRight.".into());
1544    }
1545    for k in ["overwrite", "approve"] {
1546        if given(k).is_some_and(|v| !v.is_boolean()) {
1547            return refuse(format!("`{k}` must be true or false."));
1548        }
1549    }
1550    if let Some(o) = given("output") {
1551        let ok = o.as_str().map(str::trim).is_some_and(|t| !t.is_empty() && t.chars().count() <= MAX_MACRO_REF && !t.chars().any(char::is_control));
1552        if !ok {
1553            return refuse(format!("`output` must be reference text of at most {MAX_MACRO_REF} characters, such as \"F2\" or \"Sheet2!F2\"."));
1554        }
1555    }
1556    if given("timeLimit").is_some_and(|v| !v.as_f64().is_some_and(|t| t > 0.0)) {
1557        return refuse("`timeLimit` must be a number of seconds above 0.".into());
1558    }
1559    Ok(())
1560}
1561
1562/// The run's `timeLimit` (seconds) in milliseconds, or the default.
1563fn time_limit(p: &Json) -> f64 {
1564    p.get("timeLimit").and_then(Json::as_f64).map(|t| (t * 1000.0).clamp(1.0, 600_000.0)).unwrap_or(DEFAULT_TIME_LIMIT_MS)
1565}
1566
1567/// The conflict rule for this run: `onConflict`, else the macro's own. `overwrite: true` is the
1568/// answer to the question, so it turns `ask` into `overwrite` whichever way `ask` was given.
1569fn conflict_rule(p: &Json, m: &Macro) -> Result<OnConflict> {
1570    let rule = match p.get("onConflict").and_then(Json::as_str) {
1571        Some(t) => OnConflict::parse(t)
1572            .ok_or_else(|| EngineError::Other(format!("onConflict must be ask, fail, overwrite, shiftDown or shiftRight, not '{t}'.")))?,
1573        None => m.output.on_conflict,
1574    };
1575    if rule == OnConflict::Ask && p.get("overwrite").and_then(Json::as_bool) == Some(true) {
1576        return Ok(OnConflict::Overwrite);
1577    }
1578    Ok(rule)
1579}
1580
1581/// For `macro.check`, as the run would: shifts the data, checks the footprint again, and puts
1582/// everything back. What still stands in the way, or `None`.
1583fn shifted_conflicts(s: &mut Session, fp: &Footprint, p: &Prepared, budget: &Machine, down: bool) -> Result<Option<String>> {
1584    let token = s.group_begin("Check Macro")?;
1585    let saved = Saved::take(s, p.doc);
1586    let mut spent = budget.clone();
1587    (spent.executed, spent.param_bytes) = (fp.executed, fp.param_bytes);
1588    let again = make_room(s, fp, p, down).and_then(|()| footprint(s, p, &spent));
1589    let rolled = s.group_rollback(token);
1590    saved.restore(s, p.doc);
1591    rolled?;
1592    Ok(match again {
1593        Err(e) => Some(e.to_string()),
1594        Ok(again) => match (&again.error, again.conflict_count()) {
1595            (Some(e), _) => Some(e.describe()),
1596            (None, 0) => None,
1597            (None, _) => Some(format!("Shifting the data doesn't make room ({}).", again.describe_conflicts())),
1598        },
1599    })
1600}
1601
1602/// `macro.check`: what running the macro would do, without changing anything.
1603pub fn check(s: &mut Session, name: &str, p: &Json) -> Result<Json> {
1604    let d = s.doc()?;
1605    let (doc, trust) = (d.uid, d.trust);
1606    let m = super::find(&d.wb, name)
1607        .and_then(|i| d.wb.macros.get(i))
1608        .cloned()
1609        .ok_or_else(|| EngineError::Other(format!("There is no macro called '{name}'.")))?;
1610    let mut out = json!({"macro": m.name, "ok": false, "onConflict": m.output.on_conflict.name()});
1611    let refused = |mut out: Json, e: String| {
1612        out["errors"] = json!([e]);
1613        Ok(out)
1614    };
1615    if let Err(e) = check_params(p, &m) {
1616        return refused(out, e.to_string());
1617    }
1618    let rule = match conflict_rule(p, &m) {
1619        Ok(r) => r,
1620        Err(e) => return refused(out, e.to_string()),
1621    };
1622    out["onConflict"] = json!(rule.name());
1623    let asks = match gate(&m, trust) {
1624        Ok(a) => a,
1625        Err(e) => return refused(out, e.to_string()),
1626    };
1627    let approved = s.macro_approvals.contains(&(doc, super::fingerprint(&m)));
1628    if !asks.is_empty() && !approved {
1629        // Nothing from an unapproved macro runs, not even in a dry run.
1630        out["needsApproval"] = json!(asks);
1631    }
1632    let prepared = match prepare(s, &with_output(m, p), p.get("inputs")) {
1633        Ok(pr) => pr,
1634        Err(e) => return refused(out, e.to_string()),
1635    };
1636    out["inputs"] = prepared.inputs_json(&s.doc()?.wb);
1637    if let Some(o) = prepared.output {
1638        let wb = &s.doc()?.wb;
1639        out["output"] = json!(resolve::a1(o, &Env { wb, active_sheet: wb.active_sheet(), inputs: &[], output: None, item: None }));
1640    }
1641    if !asks.is_empty() && !approved {
1642        return Ok(out);
1643    }
1644    let mut budget = Machine::new(&prepared, false);
1645    budget.time_limit_ms = time_limit(p);
1646    let fp = footprint(s, &prepared, &budget)?;
1647    // `ok`: the run would go ahead with this rule, without a question.
1648    let mut problems = Vec::new();
1649    if let Some(e) = &fp.error {
1650        problems.push(e.describe());
1651    }
1652    if let (true, Some(msg)) = (fp.rolls_back, &fp.stopped) {
1653        problems.push(format!("A Stop step ends the run and rolls it back: {msg}"));
1654    }
1655    let shifts = matches!(rule, OnConflict::ShiftDown | OnConflict::ShiftRight);
1656    if fp.conflict_count() > 0 {
1657        let what = fp.describe_conflicts();
1658        match rule {
1659            OnConflict::Ask => problems.push(format!("It would write over cells that hold data ({what}); the run asks first.")),
1660            OnConflict::Fail => problems.push(format!("It would write over cells that hold data ({what}).")),
1661            OnConflict::Overwrite => {}
1662            OnConflict::ShiftDown | OnConflict::ShiftRight => {
1663                let down = rule == OnConflict::ShiftDown;
1664                if let Some(why) = shift_refusal(&s.doc()?.wb, &fp, &prepared, down) {
1665                    problems.push(why);
1666                } else if let Some(why) = shifted_conflicts(s, &fp, &prepared, &budget, down)? {
1667                    problems.push(why);
1668                }
1669            }
1670        }
1671    }
1672    // The run spends its budget on the dry runs before it too: one, or two when it shifts.
1673    let passes: u64 = match rule {
1674        OnConflict::Overwrite => 1,
1675        _ if shifts && fp.conflict_count() > 0 => 3,
1676        _ => 2,
1677    };
1678    if fp.executed.saturating_mul(passes) > MAX_EXECUTED_STEPS {
1679        problems.push(format!("The run would execute more than {MAX_EXECUTED_STEPS} steps, counting the check before it ({} here).", fp.executed));
1680    }
1681    if fp.param_bytes.saturating_mul(passes) > MAX_PARAM_BYTES {
1682        problems.push(format!("The run would pass more than {} MB of parameters, counting the check before it.", MAX_PARAM_BYTES / (1024 * 1024)));
1683    }
1684    out["ok"] = json!(problems.is_empty());
1685    out["conflicts"] = json!(fp.conflict_count());
1686    if !problems.is_empty() {
1687        out["errors"] = json!(problems);
1688    }
1689    out["footprint"] = serde_json::to_value(&fp).unwrap_or(Json::Null);
1690    Ok(out)
1691}