Engine: macro.check and the footprint: where a macro writes, worked out before it runs

Split from the pull request below at the review's request; the code is the runner's footprint
as reviewed there, unchanged.

- `macro.run` first runs the macro in dry-run mode on the real workbook (unless the rule is
  `overwrite`) and puts everything back: the workbook, its selection and history through the
  dry run's undo group, and the session state a dry run may touch (`Saved`: the active
  workbook, open documents, the saved mark, selections, unlocked ranges, trust, clipboard,
  preferences, mode, journal and recording, queued requests, format painter, find, ruler,
  hidden windows, drawing tool and the comment panes).
- Cells it would write outside its inputs that hold data, and sheets and objects it would
  delete, are conflicts: `ask` asks before anything runs, `fail` fails, and `shiftDown` and
  `shiftRight` insert cells over the block of every written cell and check again. A shift is
  refused when it would move an input, when the macro deletes sheets or objects, or when the
  touch log is full.
- A dry run stops before a file step and before Solver; the footprint is then partial, and the
  run checks each step as it goes.
- The dry runs and the run share one clock, one time limit and one step budget.
- `macro.check` reports the inputs, the output anchor, approval needed and the footprint.
- Tests: the footprint tests that moved with this code.
This commit is contained in:
prxnvt
2026-10-25 02:33:26 -04:00
parent 5860857937
commit 0719458114
7 changed files with 869 additions and 67 deletions
+23
View File
@@ -206,6 +206,9 @@ pub struct DocState {
/// workbook is opened from a file, carried by content pasted from such a workbook, and never
/// changed by a trust decision.
pub trust: Trust,
/// While a macro's footprint is measured: every cell a command wrote, by sheet name, even
/// where it wrote what was already there.
pub touched: Option<Vec<(String, CellRef)>>,
/// The references `edit.goTo` with `record: true` went to in this document, newest first,
/// without names, each area written with its sheet (`Sheet1!B14:C22,Sheet1!F19:G30`).
pub go_to_recent: Vec<String>,
@@ -299,6 +302,7 @@ impl DocState {
content_kind: None,
scripts_trusted,
trust,
touched: None,
go_to_recent: vec![],
}
}
@@ -434,6 +438,9 @@ pub struct Session {
/// Serialized size of each journal entry, measured once when it is pushed, and their sum.
journal_sizes: Vec<usize>,
journal_bytes: usize,
/// Entries the journal has dropped from its front to stay within its limits, ever; a dry run
/// that puts the journal back needs to know how many went while it ran.
journal_dropped: u64,
pub ui_requests: Vec<UiRequest>,
/// Format painter source style (sticky when double-clicked).
pub format_painter: Option<(Arc<Workbook>, usize, RangeRef, bool)>,
@@ -558,10 +565,26 @@ impl Session {
self.journal.drain(..n);
self.journal_sizes.drain(..n);
self.journal_bytes = kept;
self.journal_dropped = self.journal_dropped.saturating_add(n as u64);
self.recording = self.recording.map(|r| r.saturating_sub(n));
}
}
/// Entries dropped from the journal's front so far, for a caller that puts the journal back.
pub(crate) fn journal_dropped(&self) -> u64 {
self.journal_dropped
}
/// Cuts the journal back to its first `keep` entries, with its size bookkeeping.
pub(crate) fn truncate_journal(&mut self, keep: usize) {
if self.journal_sizes.len() == self.journal.len() {
self.journal_sizes.truncate(keep);
self.journal_bytes = self.journal_sizes.iter().sum();
}
self.journal.truncate(keep);
self.recording = self.recording.map(|r| r.min(self.journal.len()));
}
pub fn commands(&self) -> Vec<CommandInfo> {
command_specs().iter().map(|c| c.info(self)).collect()
}