Makros: Engine- und Store-Fundament (Zellen/Zeilen/Spalten/Tabellen/Blätter per Skript)
Erster von zwei Commits (UI folgt separat): neue, explizit ausgelöste Makro-Sprache — dieselbe block-mode-Grammatik wie bei "Mit Schleife"- Funktionen (FOR/IF/RETURN), plus ein neues Schreib-Vokabular (SETVALUE, INSERTROW/DELETEROW/INSERTCOL/DELETECOL, ADDTABLE/RENAMETABLE/DELETETABLE, ADDSHEET/RENAMESHEET/DELETESHEET), adressiert mit der bestehenden Blatt!Tabelle::Zelle-Syntax. Bewusst außerhalb der normalen Neuberechnung: ein Makro läuft nie aus einer Zellformel heraus, sondern nur explizit getriggert (store.runMacro), und mutiert ausschließlich über die bereits vorhandenen Store-Primitiven (setCellInput, insertRow, addTable, addSheet, …) — Workbook/EngineHost/ DependencyGraph bleiben unverändert rein lesend. Ein ganzer synchroner Makro-Lauf wird automatisch zu einem einzigen Undo-Schritt. 50 neue Tests (Engine + Store), inkl. eines gezielten Regressionstests für die zentrale Korrektheits-Fallgrube: eine strukturelle Aktion mitten im Lauf ersetzt die Workbook-Instanz komplett, jeder Host-Zugriff muss daher frisch aufgelöst werden statt eine Referenz zu cachen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
import {
|
||||
parseFormula,
|
||||
parseFunctionBlock,
|
||||
type ActionNode,
|
||||
type BlockNode,
|
||||
type ForNode,
|
||||
type Node,
|
||||
@@ -81,6 +82,23 @@ export function expandUserFunctions(ast: Node, index: ReadonlyMap<string, Functi
|
||||
return expand(ast, index, new Map(), new Map(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link expandUserFunctions}, for a macro body (`parseMacroBlock()`'s
|
||||
* output) instead of an ordinary formula/function AST -- resolves any
|
||||
* custom-function call reachable from a macro's expressions (e.g.
|
||||
* `SETVALUE(Umsatz::A1; MARGE(Umsatz::B1; Umsatz::C1))`) the exact same way
|
||||
* a formula would, via the already-existing `expandBlock`. The store calls
|
||||
* this once per `runMacro()` before handing the block to
|
||||
* evaluator.ts's `runMacroActions` -- a nested custom-function call can
|
||||
* therefore compute a value inside a macro, but (since it's always parsed
|
||||
* with `allowWrites: false`) can never itself contain an `ActionNode`, so
|
||||
* it can never perform a write.
|
||||
*/
|
||||
export function expandMacroBlock(block: BlockNode, index: ReadonlyMap<string, FunctionDef>): BlockNode {
|
||||
if (index.size === 0) return block; // same fast path as expandUserFunctions
|
||||
return expandBlock(block, index, new Map(), new Map(), 0);
|
||||
}
|
||||
|
||||
function expand(
|
||||
node: Node,
|
||||
index: ReadonlyMap<string, FunctionDef>,
|
||||
@@ -235,6 +253,8 @@ function expandBlock(
|
||||
then: expandBlock(stmt.then, index, bodyCache, blockCache, depth),
|
||||
else: stmt.else ? expandBlock(stmt.else, index, bodyCache, blockCache, depth) : null,
|
||||
};
|
||||
case 'action':
|
||||
return { ...stmt, args: stmt.args.map((a) => expand(a, index, bodyCache, blockCache, depth)) };
|
||||
}
|
||||
}),
|
||||
};
|
||||
@@ -289,6 +309,7 @@ export function declaredBlockNames(block: BlockNode, out: string[] = []): string
|
||||
if (stmt.else) declaredBlockNames(stmt.else, out);
|
||||
break;
|
||||
case 'return':
|
||||
case 'action':
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -307,14 +328,7 @@ export function declaredBlockNames(block: BlockNode, out: string[] = []): string
|
||||
export function findInvalidForSource(block: BlockNode): ForNode | null {
|
||||
for (const stmt of block.statements) {
|
||||
if (stmt.kind === 'for') {
|
||||
if (
|
||||
stmt.source.kind !== 'param' &&
|
||||
stmt.source.kind !== 'cell' &&
|
||||
stmt.source.kind !== 'range' &&
|
||||
stmt.source.kind !== 'namedref'
|
||||
) {
|
||||
return stmt;
|
||||
}
|
||||
if (!isReferenceShaped(stmt.source)) return stmt;
|
||||
const nested = findInvalidForSource(stmt.body);
|
||||
if (nested) return nested;
|
||||
} else if (stmt.kind === 'ifstmt') {
|
||||
@@ -325,6 +339,35 @@ export function findInvalidForSource(block: BlockNode): ForNode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Shared by {@link findInvalidForSource} and {@link findInvalidActionTarget}:
|
||||
* can this node, once parameters/locals are substituted at call time,
|
||||
* possibly resolve to a cell/range address at all? (A computed value --
|
||||
* `2+2`, a function call, ... -- never can, no matter what's substituted
|
||||
* in.) */
|
||||
function isReferenceShaped(node: Node): boolean {
|
||||
return node.kind === 'param' || node.kind === 'cell' || node.kind === 'range' || node.kind === 'namedref';
|
||||
}
|
||||
|
||||
/** {@link findInvalidForSource}'s counterpart for a macro's `SETVALUE`
|
||||
* target (its first argument) instead of a FOR source -- same reasoning,
|
||||
* checked at save time by store/document.ts#validateMacro. */
|
||||
export function findInvalidActionTarget(block: BlockNode): ActionNode | null {
|
||||
for (const stmt of block.statements) {
|
||||
if (stmt.kind === 'action') {
|
||||
if (stmt.name.toUpperCase() === 'SETVALUE' && stmt.args.length > 0 && !isReferenceShaped(stmt.args[0])) {
|
||||
return stmt;
|
||||
}
|
||||
} else if (stmt.kind === 'for') {
|
||||
const nested = findInvalidActionTarget(stmt.body);
|
||||
if (nested) return nested;
|
||||
} else if (stmt.kind === 'ifstmt') {
|
||||
const nested = findInvalidActionTarget(stmt.then) ?? (stmt.else && findInvalidActionTarget(stmt.else));
|
||||
if (nested) return nested;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** {@link collectCalledFunctionNames}, for a `kind: 'block'` body: runs it
|
||||
* over every leaf expression a `BlockNode` contains (each statement's
|
||||
* value/condition/source, recursing into nested FOR/IF bodies) and merges
|
||||
@@ -349,6 +392,9 @@ export function collectCalledFunctionNamesInBlock(
|
||||
collectCalledFunctionNamesInBlock(stmt.then, out);
|
||||
if (stmt.else) collectCalledFunctionNamesInBlock(stmt.else, out);
|
||||
break;
|
||||
case 'action':
|
||||
stmt.args.forEach((a) => collectCalledFunctionNames(a, out));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
|
||||
Reference in New Issue
Block a user