Add FOR-loop "code" bodies to user-defined functions (kind: 'block')
A custom function body could previously only be a single expression -- no way to iterate over a range's elements to build something like PRODUCT (multiply every cell together), since none of the built-ins (SUM/AVG/MIN/MAX/COUNT) do that and a body can only compose existing functions, not define a new reduction. This adds a second body kind, 'block': a sequence of `name = expression` assignments and `FOR var IN source DO name = expression` loops, ending in `RETURN expression`. Flat scope -- every introduced name stays visible for the rest of the body, including after its own loop, holding its last iteration's value. No runtime loop in the evaluator: a loop's source size is already known once the caller's real argument is substituted in, so userFunctions.ts's new compileBlock() unrolls every FOR loop into a single nested expression (a left fold) at the same parse-time inlining point Phase 1 already used -- the result is exactly the same shape of static AST as any other inlined function call, so evaluator.ts and graph.ts need no changes, dependency edges on every unrolled cell are picked up automatically, and a bounded MAX_UNROLL_STEPS (10,000) keeps it crash/hang-free. parser.ts's parseFunctionBlock() parses the new grammar (a purely additive EQUALS lexer token; FOR/IN/DO/RETURN are plain case-matched IDENTs, not new reserved tokens) into a BlockNode kept outside the existing Node union, so no existing switch needs a new case. Store: CustomFunction/FunctionDef gain `kind: 'expression' | 'block'` (defaulted for every function saved before this existed, including via JSON import). validateCustomFunction validates the new grammar's local/ loop-variable names the same way as parameters (not cell-shaped, no reserved words, no shadowing a parameter -- but reusing the same local across statements, the accumulator pattern, is correctly not a collision) and that every FOR source is structurally a parameter, cell, or range. UI: a "𝑓 Funktionen" form toggle between "Einfache Formel" and "Mit Schleife", with its own placeholder/hint (a PRODUCT example) and a small badge on block-kind rows in the list. 237 tests passing (34 new), vue-tsc/eslint/build clean. Verified live in the running app: defined PRODUCT via the loop UI, called =PRODUCT(Umsatz!B2:E6), confirmed the correct value and recalculation on a change to a cell inside the range, and an inline rejection for an invalid FOR source at save time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ Vorbild von **Apple Numbers**. Zwei Dinge stehen im Mittelpunkt:
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # Vite-Devserver
|
||||
npm run test # Vitest-Suite (Engine + Store + IO), 200 Tests
|
||||
npm run test # Vitest-Suite (Engine + Store + IO), 237 Tests
|
||||
npm run build # Typecheck + Produktionsbuild
|
||||
npm run lint # ESLint (flat config)
|
||||
```
|
||||
@@ -193,6 +193,48 @@ eingebauter Funktionsname: `=MARGE(Umsatz!B2; Umsatz!C2)`.
|
||||
Aufruf), lösen bei Änderung korrekt eine Neuberechnung aus.
|
||||
- Vollständig **undo-/redo-fähig** und Teil des JSON-Exports/-Imports.
|
||||
|
||||
#### Mit Schleife: FOR-Code statt einer einzelnen Formel
|
||||
|
||||
Der „Art"-Umschalter im Formular bietet neben **„Einfache Formel"** (s. o.)
|
||||
auch **„Mit Schleife"** — echter Code statt eines einzelnen Ausdrucks, für
|
||||
Fälle, die eine einzelne Formel nicht ausdrücken kann: über die Elemente
|
||||
eines Bereichs selbst iterieren (z. B. ein Produkt aller Zellen, nicht nur
|
||||
Summe/Mittel/Min/Max). Das Code-Feld akzeptiert eine Folge aus:
|
||||
|
||||
- **Zuweisungen** — `name = ausdruck`.
|
||||
- **FOR-Schleifen** — `FOR var IN bereich DO name = ausdruck`; `bereich` ist
|
||||
ein Parameter, eine Zelle oder ein Bereich (kein berechneter Ausdruck).
|
||||
|
||||
— beliebig oft, beliebig gemischt, zuletzt **`RETURN ausdruck`**. Jeder
|
||||
eingeführte Name (Zuweisungsziel oder Schleifenvariable) bleibt für den
|
||||
**restlichen** Code sichtbar, auch nach dem Ende „seiner" Schleife (mit dem
|
||||
Wert der letzten Iteration) — flacher Namensraum, kein Block-Scoping.
|
||||
`FOR`/`IN`/`DO`/`RETURN` sind reservierte Wörter, nicht als Parameter-/
|
||||
Variablenname erlaubt.
|
||||
|
||||
Beispiel — Produkt aller Elemente eines Bereichs (`PRODUCT` gibt es nicht als
|
||||
eingebaute Funktion, nur `SUM`/`AVG`/`MIN`/`MAX`/`COUNT`):
|
||||
|
||||
```
|
||||
ergebnis = 1
|
||||
FOR x IN bereich DO ergebnis = ergebnis * x
|
||||
RETURN ergebnis
|
||||
```
|
||||
|
||||
aufgerufen als `=PRODUCT(Umsatz!B2:E6)`.
|
||||
|
||||
**Keine echte Laufzeit-Schleife im Evaluator:** Da die Größe eines Bereichs
|
||||
beim Aufruf bereits feststeht, wird eine `FOR`-Schleife beim Einsetzen des
|
||||
Funktionsaufrufs vollständig **abgerollt** — jede Iteration wird zu einem
|
||||
eigenen, ineinander verschachtelten Ausdrucks-Teilbaum (ein Links-Fold). Am
|
||||
Ende steht wieder eine ganz normale, statische Formel-AST — Evaluator und
|
||||
Abhängigkeitsgraph sehen keinen Unterschied zu einer von Hand ausgeschriebenen
|
||||
Formel, Neuberechnung bei Änderung *jeder* im Bereich enthaltenen Zelle
|
||||
funktioniert automatisch korrekt. Kein Endlos-Risiko: mehr als insgesamt
|
||||
10.000 abgerollte Zellen (über alle Schleifen einer Funktion) ergeben `#ERR`
|
||||
statt zu hängen. Details: [`src/engine/README.md`](src/engine/README.md)s
|
||||
„Schleifen"-Abschnitt.
|
||||
|
||||
### Zell-Formatierung
|
||||
|
||||
Auswahl (Zelle, Bereich, Mehrfachauswahl, ganze Zeile/Spalte über den Kopf) → im
|
||||
@@ -445,7 +487,7 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||||
|
||||
## Tests
|
||||
|
||||
`npm run test` — Vitest, 200 Tests:
|
||||
`npm run test` — Vitest, 237 Tests:
|
||||
|
||||
- **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^`
|
||||
rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`,
|
||||
@@ -521,6 +563,30 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||||
Löschen lässt bestehende Aufrufe zu `#NAME?` werden, Undo/Redo einer
|
||||
Funktionsänderung, voller JSON-Round-Trip, `customFunctions: []` bei
|
||||
v1→v2-Migration.
|
||||
- **„Mit Schleife" (FOR-Code)** — Parser-Ebene (`parseFunctionBlock`):
|
||||
eine Schleife (Akkumulator-Muster), mehrere unabhängige Schleifen
|
||||
nacheinander, mehrere Zuweisungen vor/zwischen Schleifen, ein Name aus
|
||||
einer früheren Anweisung in einer späteren Schleife nutzbar; Fehler bei
|
||||
fehlendem `RETURN`, `FOR` ohne `DO`/`IN`, fehlendem `=`, Selbstbezug im
|
||||
eigenen Initialisierer. Engine-Ebene (`compileBlock`): korrektes Abrollen
|
||||
(ein Bereich → verschachtelte Ausdrücke in Zellreihenfolge), ein einzelner
|
||||
Zellbezug als Quelle → genau eine Iteration, die Schleifenvariable/das
|
||||
letzte Zuweisungsziel bleiben danach mit ihrem letzten Wert sichtbar,
|
||||
verschachtelter Aufruf einer anderen Funktion im Schleifenkörper wird mit-
|
||||
expandiert, Arity-Prüfung gilt auch für „Mit Schleife"-Funktionen, eine
|
||||
FOR-Quelle, die nach Parametersubstitution kein Zell-/Bereichsbezug ist,
|
||||
wirft, mehr als 10.000 abgerollte Zellen wirft. Workbook-Integration:
|
||||
korrekter Wert und Neuberechnung bei Änderung einer Zelle *innerhalb* des
|
||||
übergebenen Bereichs, Überschreitung des Abroll-Limits und eine ungültige
|
||||
FOR-Quelle ergeben `#ERR` statt eines Absturzes. Store-Ebene: reservierte
|
||||
Wörter (`FOR`/`IN`/`DO`/`RETURN`) als Parameter- oder lokaler Name
|
||||
abgelehnt, zellartiger lokaler Name abgelehnt, Wiederverwendung desselben
|
||||
lokalen Namens über mehrere Anweisungen (das Akkumulator-Muster) ist
|
||||
**keine** Kollision, ein lokaler Name, der einen Parameter überschattet,
|
||||
wird abgelehnt, eine berechnete FOR-Quelle wird abgelehnt, Funktions-Zyklus
|
||||
durch einen Schleifenkörper wird erkannt, Undo/Redo, `kind` im JSON-
|
||||
Round-Trip, ein älteres gespeichertes Dokument ohne `kind`-Feld defaultet
|
||||
auf `'expression'`.
|
||||
- **Blatt-Qualifier „Blatt!Tabelle::Zelle"** — Parser-Ebene: die explizite
|
||||
3-teilige Form (bar und quoted, in Bereichen/Funktionsargumenten), volle
|
||||
Rückwärtskompatibilität der 2-teiligen Form (`X!Y`, `X::Y`, auch ein
|
||||
@@ -564,6 +630,10 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||||
Ergebnisses im Bearbeiten-Dialog, variadische Parameter/Default-Werte, und
|
||||
automatisches Nachführen bestehender Aufrufe bei Umbenennung (Verhalten
|
||||
entspricht bewusst dem einer Tabellen-Umbenennung, s. o.).
|
||||
- **„Mit Schleife"-Code** bewusst ohne: verschachtelte Schleifen (`FOR`
|
||||
innerhalb einer `DO`-Klausel), bedingte Anweisungen (`IF`), echtes
|
||||
Block-Scoping (Namen bleiben absichtlich bis zum Ende des Codes sichtbar),
|
||||
frühzeitiges `RETURN` mitten in der Anweisungsfolge (nur am Ende erlaubt).
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
|
||||
Reference in New Issue
Block a user