Main adds UiRequest::InsertName (Paste Name, Use in Formula), which puts a name in the cell
editor; a macro step drops it, as it drops a request to start editing a cell.
The touch log keeps at most a million writes; past that, a write that kept a
cell's value is missing from the footprint, so a shift derived from the
block of written cells could split a row the macro writes. The footprint
now says when the log filled up (logFull in macro.check), and a shift is
refused then, as it is when the macro deletes sheets or objects. Regression
test with a million-cell fill in a dry run.
Every nested command a dry run executes is journaled, so a long macro pushes
the journal past its limit and the journal drops its oldest entries. The
restore cut the journal back to its old length, which then kept entries from
the dry run and pointed Record Actions at the wrong place. The session now
counts the entries its journal has dropped (journal_dropped) and truncates
with its size bookkeeping (truncate_journal); the restore keeps the old
length less what was dropped meanwhile and moves the recording's start back
by the same count. Regression test with 12,000 nested steps.
Four fixes from a review of the runner, each with a regression test that
fails without it:
- A shift inserts cells over the block of every written cell, which moves
that block's columns (or rows) from there on. When a bound input lay in
the way, its data moved and sheared while the run kept the old coordinates
and reported success. The shift is now refused, naming the input and its
place. The output is not counted: it is where the macro writes, and the
second footprint catches data a shift moved into it.
- A dry run or a refused run left a clean workbook dirty: view.zoom,
sheet.activate and the view modes move the saved mark to the current
workbook on a clean document, and the rollback put the workbook back but
not the mark. Saved keeps and restores it.
- overwrite: true answers the question however ask was given; with an
explicit onConflict: ask the answer was ignored and the question came back
forever.
- The dry runs and the run share one step budget as they share one clock;
each phase counted its own 100,000 steps before. ran reports the run's
own steps.
Approvals were stored as (document, FNV-1a hash of the macro's JSON).
FNV-1a is not collision resistant, so a crafted workbook could hold a
harmless macro and a different one with the same hash, and approving
the first would let the second run without asking.
The key is now the macro's canonical JSON text itself (object keys
sorted), so two different macros never share an approval.
macros::hash is replaced by macros::fingerprint.
macro.debug runs a macro one step at a time. start checks the macro as
macro.run does (policy, approval, inputs, footprint and output rule)
and pauses before the first step; step runs one step; continue runs to
the next breakpoint; runTo runs to a chosen step path, stopping at
breakpoints on the way; stop ends the run and keeps its changes or
rolls them back; state reports where it is. Breakpoints are the steps'
own flags plus a list of paths given with any action (breakpointsOnly
makes the list replace the flags), and they stop in every pass of a
Repeat.
The run lives on the session between calls, inside an undo group that
spans them (group_begin and group_end), so a debugged run is one undo
step "Run Macro: <name>", as a normal run is. A failing step ends the
run and rolls it back; so does a Stop step that asks for a rollback.
While a macro is being debugged, edits of the workbooks the run touched
(its own, and any other a step changed) are refused at the top level
with "Finish or stop the macro first"; other workbooks stay editable,
and selecting and queries still work. Every answer gives the next
step's path, kind, command, parameters as they would run now (tokens
resolved), the Repeat pass, the parameters of the step that just ran,
the steps executed and the cells changed so far. Each action gets its
own time limit.
If a command panics while a step runs, the session's panic guard rolls
the debugged run back as well, so its undo group never stays open.
footprint always ran its dry run with the default 60 second limit, so
macro.run with timeLimit 5 could run for over two minutes (a dry run,
then another after a shift) and fail with "ran longer than 60 seconds".
footprint now takes the run's clock and limit. make_way starts the
run's clock before the dry runs, so the checks and the run share one
limit, and macro.check takes timeLimit too.
The footprint dry run stops at an approved file step and at Solver, but
make_way took a partial footprint with no conflicts as proof that the
run was safe, also after a shift. A macro that exported a CSV and then
set B1 ran under onConflict fail or ask and overwrote B1 without asking.
When the footprint is partial and the rule is not overwrite, the run's
machine now checks every step as it runs: a step that writes over
cells, sheets or objects that held data when the run started stops the
run and rolls it back, with a confirmation under ask and an error
otherwise. make_way now returns the machine for the run along with
whether it shifted.
A path given as a token (`{"$ref": "Input"}` or `{{Input}}`) became a file name only when the
step ran, after the policy had passed it, so a macro without permissions or approval could write
a file, even from macro.check. Exports also wrote to blank paths the policy ignored. Any `path`
other than null now needs the permission and approval, and the runner checks the policy again on
the resolved parameters before the command runs.
macro.run runs a macro as one undo step ("Run Macro: <name>"); an error
at any step rolls everything back and names the step and its path (3,
2.1 inside a Repeat, 4.then.1 inside an If). It works in four parts:
- Policy (macro_policy). File, app, window, automate and macro commands
and Undo and Redo never run in a macro. Reading or writing files
(data.getData, data.fromTextCsv, insert.picture, Page Layout
Background, or an export with a path) needs the command in the
macro's permissions and the person's approval. A macro in a workbook
opened from a file asks before it runs. Approval holds for the
session until the macro's content changes; it is asked for with the
new EngineError::Approve and given with approve: true. A step that
would open a dialog or a web page fails instead.
- Inputs. Each declared input is bound (given, its default, or the
selection for the first one), whole columns shrink to the used range,
and the shape and column types are checked with messages that name
the cells: "Input column 2 (Amount) must hold numbers; C4 holds text".
- Footprint. Unless the macro may overwrite, it first runs in dry-run
mode on the real workbook inside an undo group that is rolled back,
with the session's clipboard, journal, windows and the document's
trust put back. CellStore::diff on every sheet, plus the cells
commands wrote (DocState::touched), sheets and objects, gives what it
would write outside its inputs; cells that only recalculated do not
count. Writing over data, deleting a sheet or deleting an object is a
conflict: ask (the new EngineError::Confirm, so programmatic runs fail
unless they pass onConflict or overwrite), fail, overwrite, or shift
the data down or right with Insert Cells and check again. A step that
would fail, for example past the edge of the sheet, fails the run
before anything changes. File steps and Solver stop the dry run early
and are reported.
- Steps run on a machine with an explicit frame stack: Repeat over the
rows or columns of a range with Row or Column bound, If on one cell
with Excel's comparisons, and Stop, which keeps the changes or rolls
them back. A run executes at most 100,000 steps within a time limit
(60 s by default, timeLimit to change; the web build has no clock, so
only the step limit applies there).
macro.check reports the bound inputs, the output anchor, the approval
needed, the footprint and the conflicts without changing anything;
nothing from an unapproved macro runs, not even as a dry run.