Real control flow for "Mit Schleife" functions: DO/END, IF/ELSE/END, RETURN anywhere
Replaces the previous FOR-only, single-statement-body, static-unrolling design with genuine control flow: FOR var IN source DO <block> END loop bodies with any number of statements (including nested loops/IFs), IF cond THEN <block> [ELSE <block>] END branching (nestable in any block), and RETURN as an ordinary statement usable anywhere -- it aborts execution immediately with its value, including breaking out of a loop early. This required a real, if small, architecture change: an IF branch or an early RETURN depends on actual cell values, which the previous "unroll everything into one static formula at parse time" approach can't resolve (loop *counts* are knowable up front from a range's size; branch *outcomes* aren't). A kind: 'block' call is now inlined only as far as resolving nested function calls (expandBlock, parse-time, same inside-out principle as before) and wrapped in a new BlockCallNode; its own control flow is interpreted at evaluation time by a small tree-walking interpreter added to evaluator.ts (execBlock/execStatement, a thrown ReturnSignal for early exit), reusing userFunctions.ts's substitute() (now exported) to resolve parameter/local-variable names against the current environment. Dependency tracking stays fully correct and conservative: collectRefs() walks into a BlockCallNode's block (new collectRefsInBlock), so a cell referenced only in a branch that doesn't happen to run is still a tracked precedent -- same principle as an ordinary IF() in any spreadsheet. Also added: comparison operators (= <> > < >= <=, result 1/0) as a new, loosest-binding precedence level in the *general* expression grammar (not just inside a block's IF) -- needed for IF conditions to be useful at all. A definition-time static check (blockAlwaysReturns) rejects a function at save time when it's not obviously guaranteed to reach RETURN on every path (an IF without an ELSE, or one where only one branch returns); the interpreter's own runtime "ended without RETURN" -> #ERR remains the actual enforcement for cases the static check can't see (e.g. a loop over an empty range). Nested loops (FOR inside FOR) and nested IFs work as an emergent property of the recursive statement grammar -- verified with dedicated tests, not just assumed. 257 tests passing (30 net new/rewritten for this change), vue-tsc/ eslint/build clean. Verified live in the running app: a FIRSTOVER(range; threshold) function using a FOR loop with a nested IF and an early RETURN, confirmed it returns the first match, confirmed changing a cell *past* the match point doesn't affect the result while changing the match point itself does, and confirmed a function without a guaranteed RETURN is rejected inline at save time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+130
-66
@@ -16,11 +16,11 @@ genau die genannte Tabelle. Siehe "Blatt-Qualifier" weiter unten.
|
||||
|----------------|---------|
|
||||
| `lexer.ts` | Tokenizer. Flacher Token-Strom, keine strukturellen Entscheidungen. Kennt `!` und `::`. |
|
||||
| `parser.ts` | Recursive-Descent → AST. Grammatik als EBNF-Kommentarblock im Kopf der Datei. |
|
||||
| `evaluator.ts` | AST + `EngineHost` → `CellValue`. Numerisches Auswertungsmodell, Fehler-Propagierung. |
|
||||
| `evaluator.ts` | AST + `EngineHost` → `CellValue`. Numerisches Auswertungsmodell, Fehler-Propagierung. Enthält auch den kleinen Interpreter für `kind: 'block'`-Funktionsaufrufe (`BlockCallNode`) – siehe „Schleifen" unten. |
|
||||
| `graph.ts` | Dokumentweiter Abhängigkeitsgraph: `affected()`, `topoOrder()`, Zirkel-Erkennung (iterative DFS, white/grey/black). |
|
||||
| `refs.ts` | A1 ↔ `(col,row)` inkl. `$`-Sperrachsen, Spaltenbuchstaben, Bereichs-Expansion, `F4`-Zyklus (`cycleReferenceLocks`). |
|
||||
| `functions.ts` | `SUM/AVG/MIN/MAX/COUNT` (+ Alias `AVERAGE`) als Tabelle `{ NAME → fn(values) }`. Eingebaute Namen sind nie überschreibbar. |
|
||||
| `userFunctions.ts` | Benutzerdefinierte Funktionen (dokumentweite Bibliothek, angelegt über die UI — siehe Haupt-`README.md`). `expandUserFunctions()` setzt jeden Aufruf beim Parsen der aufrufenden Zelle als Kopie des rekursiv expandierten, parametersubstituierten Funktionskörpers in deren AST ein; `compileBlock()` rollt zusätzlich `kind: 'block'`-Funktionskörper (FOR-Schleifen) zu einem einzigen Ausdruck ab, siehe „Schleifen" unten. |
|
||||
| `userFunctions.ts` | Benutzerdefinierte Funktionen (dokumentweite Bibliothek, angelegt über die UI — siehe Haupt-`README.md`). `expandUserFunctions()` setzt einen `kind: 'expression'`-Aufruf beim Parsen der aufrufenden Zelle als Kopie des rekursiv expandierten, parametersubstituierten Funktionskörpers in deren AST ein; einen `kind: 'block'`-Aufruf (FOR/IF/RETURN-Code) verpackt sie stattdessen in einen `BlockCallNode` mit bereits aufgelösten verschachtelten Aufrufen, siehe „Schleifen" unten. |
|
||||
| `workbook.ts` | Bindeglied: flache Liste von **Tabellen** + rohe Eingaben + ASTs + Werte + Graph. Inkrementelle Neuberechnung. Implementiert `EngineHost`. Ruft `expandUserFunctions()` direkt nach `parseFormula()`. |
|
||||
| `types.ts` | Gemeinsame Typen, `FormulaError`, `keyOf()`. |
|
||||
|
||||
@@ -33,7 +33,16 @@ Instanz in ein `reactive()` und spiegelt berechnete Werte über einen
|
||||
```ebnf
|
||||
formula = "=" expression ;
|
||||
|
||||
expression = term { ("+" | "-") term } ; (* links-assoziativ *)
|
||||
expression = comparison ;
|
||||
comparison = additive [ ("="|"<>"|">"|"<"|">="|"<=") additive ] ; (* höchstens
|
||||
ein Vergleich, nicht
|
||||
verkettbar; Ergebnis
|
||||
1/0 (siehe Auswertungs-
|
||||
modell unten) *)
|
||||
additive = term { ("+" | "-") term } ; (* links-assoziativ; die
|
||||
alte "expression"-Regel,
|
||||
bevor Vergleiche
|
||||
existierten *)
|
||||
term = unary { ("*" | "/") unary } ; (* links-assoziativ *)
|
||||
unary = ("+" | "-") unary
|
||||
| power ;
|
||||
@@ -68,7 +77,7 @@ QUOTED = /' ( [^'] | '' )* '/ (* Tabellenname mit Leer-/Sonderzeichen *
|
||||
CELL = /\$?[A-Za-z]{1,3}\$?[0-9]{1,7}/ ;
|
||||
```
|
||||
|
||||
Präzedenz, schwach → stark: `+ -` < `* /` < unäres `+ -` < `^`.
|
||||
Präzedenz, schwach → stark: Vergleich (`= <> > < >= <=`) < `+ -` < `* /` < unäres `+ -` < `^`.
|
||||
|
||||
`$` sperrt eine Referenz-Achse: `$A1` (Spalte fix), `A$1` (Zeile fix), `$A$1`
|
||||
(beide fix) vs. `A1` (relativ). Für die **Auswertung** ist das irrelevant – sie
|
||||
@@ -151,84 +160,139 @@ derselbe Pfad wie zuvor: der `FuncNode` bleibt unverändert stehen,
|
||||
|
||||
Eine zweite, eigene Grammatik nur für Funktionskörper mit `kind: 'block'`
|
||||
(neben der oben beschriebenen Ausdrucks-Form, `kind: 'expression'`, die
|
||||
unverändert bleibt):
|
||||
unverändert bleibt) – mit echtem Kontrollfluss:
|
||||
|
||||
```ebnf
|
||||
functionBlock = { statement } "RETURN" expression ;
|
||||
statement = assignment | forLoop ;
|
||||
functionBlock = block ; (* ein Block = die ganze Funktion *)
|
||||
block = { statement } ;
|
||||
statement = assignment | forLoop | ifStmt | returnStmt ;
|
||||
assignment = IDENT "=" expression ;
|
||||
forLoop = "FOR" IDENT "IN" expression "DO" assignment ;
|
||||
forLoop = "FOR" IDENT "IN" expression "DO" block "END" ;
|
||||
ifStmt = "IF" expression "THEN" block [ "ELSE" block ] "END" ;
|
||||
returnStmt = "RETURN" expression ;
|
||||
```
|
||||
|
||||
`FOR`/`IN`/`DO`/`RETURN` sind – wie Funktionsnamen – gewöhnliche `IDENT`-
|
||||
Token, case-insensitiv per Wortvergleich erkannt (`Parser.isKeyword`), kein
|
||||
eigener Lexer-Token dafür nötig; `=` als Zuweisung ist ein neuer, rein
|
||||
additiver `EQUALS`-Token (vorher ein `LexError`, kann also keine vorher
|
||||
gültige Formel beeinflussen). `parseFunctionBlock(src, paramNames)`
|
||||
(`parser.ts`) parst sequenziell: jeder eingeführte Name (Zuweisungsziel oder
|
||||
Schleifenvariable) wird sofort zu `Parser.paramNames` hinzugefügt – **flacher
|
||||
Namensraum**, kein Scope-Stack, ein Name bleibt für den *Rest* des Blocks
|
||||
sichtbar (auch nach seiner eigenen Schleife). Ergebnis ist ein `BlockNode`
|
||||
(`statements: (AssignNode | ForNode)[]`, `ret: Node`) – bewusst **nicht**
|
||||
Teil des `Node`-Unions, dadurch braucht keine bestehende
|
||||
`switch (node.kind)`-Stelle (Evaluator, `collectRefs`, `substitute`) einen
|
||||
neuen Fall.
|
||||
`FOR`/`IN`/`DO`/`END`/`IF`/`THEN`/`ELSE`/`RETURN` sind – wie Funktionsnamen –
|
||||
gewöhnliche `IDENT`-Token, case-insensitiv per Wortvergleich erkannt
|
||||
(`Parser.isKeyword`), kein eigener Lexer-Token dafür nötig; `=` als Zuweisung
|
||||
nutzt den `EQUALS`-Token (rein additiv – vorher ein `LexError`, kann also
|
||||
keine vorher gültige Formel beeinflussen; derselbe Token dient in
|
||||
`expression()` als Gleichheits-Vergleich, die beiden Grammatiken kollidieren
|
||||
nicht, weil eine Zuweisung nur an Anweisungs-Anfang steht). `RETURN` ist eine
|
||||
gewöhnliche **Anweisung**, keine feste Blockendung – sie darf überall
|
||||
stehen, auch verschachtelt in `FOR`/`IF`.
|
||||
|
||||
**Keine Laufzeit-Schleife.** `userFunctions.ts#compileBlock(block, params,
|
||||
args)` kompiliert einen Block – mit den *echten* Argumenten des Aufrufers,
|
||||
nicht mehr symbolisch – zu einem einzigen Ausdrucks-`Node`, bevor er in
|
||||
`expand()`s `func`-Fall landet:
|
||||
`parseFunctionBlock(src, paramNames)` (`parser.ts`) parst rekursiv (`parse-
|
||||
StatementBlock` ruft sich über `FOR`/`IF`-Körper selbst wieder auf): jeder
|
||||
eingeführte Name (Zuweisungsziel oder Schleifenvariable) wird sofort zu
|
||||
`Parser.paramNames` hinzugefügt – **flacher Namensraum**, kein Scope-Stack,
|
||||
ein Name bleibt für den *Rest* der Funktion sichtbar, auch nach seinem
|
||||
eigenen `FOR`/`IF`. `AssignNode`/`ForNode`/`IfNode`/`ReturnNode`/`BlockNode`
|
||||
sind bewusst **nicht** Teil des `Node`-Unions – nur der eine Knoten, der
|
||||
tatsächlich in einer normalen Zell-AST landet, ist es:
|
||||
|
||||
1. `bound`-Umgebung startet mit den Parametern (Name → Argument-`Node`).
|
||||
2. Jede `assignment` ersetzt ihren Wert mittels der **bestehenden**
|
||||
`substitute()` gegen `bound` und aktualisiert `bound`.
|
||||
3. Jede `forLoop` löst ihre Quelle (nach Substitution) über `rangeOrCellTo-
|
||||
CellNodes()` in eine konkrete Zellliste auf (`expandRange`, wie
|
||||
`evaluator.ts#evalArg` es für ein Bereichsargument tut) – ein
|
||||
Einzelzellbezug ergibt genau eine Iteration. Für jede Zelle, in
|
||||
Zellreihenfolge: die Schleifenvariable zeigt auf diese Zelle, der
|
||||
Zuweisungskörper wird gegen die **aktuelle** `bound`-Umgebung substituiert
|
||||
(sieht also den Wert der *vorherigen* Iteration) – ein Links-Fold, ein
|
||||
verschachtelter Ausdrucks-Teilbaum pro Zelle. Schleifenvariable und
|
||||
Zuweisungsziel bleiben danach in `bound` (flacher Namensraum: sichtbar mit
|
||||
ihrem letzten Wert für den Rest des Blocks).
|
||||
4. `RETURN`s Ausdruck wird zuletzt gegen die finale `bound`-Umgebung
|
||||
substituiert – das Ergebnis ist der komplette, statische Rückgabewert von
|
||||
`compileBlock`.
|
||||
```ts
|
||||
interface BlockCallNode {
|
||||
kind: 'blockcall';
|
||||
block: BlockNode; // bereits: verschachtelte Aufrufe anderer Funktionen aufgelöst
|
||||
args: Node[]; // die echten Argument-Ausdrücke des Aufrufers
|
||||
paramNames: string[];
|
||||
label: string; // für Fehlermeldungen
|
||||
}
|
||||
```
|
||||
|
||||
Das Ergebnis kann noch unaufgelöste `FuncNode`s enthalten (verschachtelte
|
||||
Aufrufe im Schleifenkörper) – deshalb läuft es direkt danach noch einmal
|
||||
durch `expand()` (eine Ebene tiefer), genau wie jeder andere Funktionskörper.
|
||||
`MAX_EXPANSION_DEPTH` gilt dadurch unverändert auch für verschachtelte
|
||||
`kind: 'block'`-Aufrufe.
|
||||
**Warum kein Abrollen mehr (wie noch bei einer früheren Version dieses
|
||||
Features):** ein `IF`-Zweig oder ein bedingtes `RETURN` hängt vom
|
||||
tatsächlichen Zellwert ab – das lässt sich nicht mehr rein aus der
|
||||
Bereichsgröße vorausberechnen. `userFunctions.ts#expand()`s `func`-Fall löst
|
||||
für `kind: 'block'` deshalb nur noch **verschachtelte Funktionsaufrufe**
|
||||
innerhalb des Blocks auf (`expandBlock()`, läuft wie `expand()` durch jede
|
||||
Anweisung/jeden verschachtelten `FOR`/`IF`-Körper, mit denselben "von innen
|
||||
nach außen"-Regeln wie bisher) und verpackt das Ergebnis unverändert in einen
|
||||
`BlockCallNode` – die *eigenen* Parameter dieser Funktion bleiben als
|
||||
`ParamNode` im Block stehen, erst zur Auswertungszeit gebunden.
|
||||
|
||||
**Kein Endlos-Risiko:** `MAX_UNROLL_STEPS` (10.000, Summe aller Schleifen
|
||||
eines `compileBlock`-Aufrufs) wirft einen `Error` bei Überschreitung –
|
||||
`#ERR`, wie jeder andere `expandUserFunctions`-Fehler. Ein `forLoop`, dessen
|
||||
(substituierte) Quelle weder eine Zelle noch ein Bereich ist, wirft ebenso
|
||||
(`rangeOrCellToCellNodes`); der Store prüft das bereits strukturell beim
|
||||
**Speichern** (`findInvalidForSource`, exportiert), bevor es je zu einem
|
||||
echten Aufruf kommt.
|
||||
**Der Interpreter (`evaluator.ts`)** läuft, wenn `evalScalar` auf einen
|
||||
`blockcall`-Knoten trifft:
|
||||
|
||||
- `evaluateBlockCall(node, host)` baut die Startumgebung `env: Map<string,
|
||||
Node>` aus `paramNames`/`args` und ruft `execBlock`/`execStatement` auf.
|
||||
- Eine `assignment`/`return`/`ifstmt`-Bedingung wird über `substitute(expr,
|
||||
env)` (aus `userFunctions.ts`, dafür **exportiert**) gegen `env` aufgelöst
|
||||
und dann ganz normal `evalScalar`-ausgewertet; das Ergebnis wird (als
|
||||
Literal-`Node`) zurück in `env` geschrieben.
|
||||
- Eine `forLoop` löst ihre (substituierte) Quelle über `rangeOrCellToCell-
|
||||
Nodes()` in eine konkrete Zellliste auf (`expandRange`, wie
|
||||
`evalArg` es für ein Bereichsargument tut) – ein Einzelzellbezug ergibt
|
||||
genau eine Iteration – und führt `body` (ein vollständiger, ggf.
|
||||
verschachtelter `BlockNode`) einmal je Zelle aus, in Zellreihenfolge.
|
||||
Schleifenvariable und alle im Körper zugewiesenen Namen bleiben danach in
|
||||
`env` (flacher Namensraum: sichtbar mit ihrem letzten Wert für den Rest der
|
||||
Ausführung).
|
||||
- `return` wirft ein `ReturnSignal` (Wert), das nur `evaluateBlockCall`
|
||||
fängt – ein Standard-"Unwind zu einer bekannten Grenze"-Muster für frühen
|
||||
Ausstieg. Das bricht **sofort** jede laufende Schleife/jedes `IF` ab, egal
|
||||
wie tief verschachtelt.
|
||||
- Läuft die Ausführung durch, ohne dass je `ReturnSignal` geworfen wurde,
|
||||
wirft `evaluateBlockCall` selbst einen `Error` ("… endet ohne RETURN").
|
||||
|
||||
Jeder `Error` hier (Schritt-Limit, ungültige FOR-Quelle, kein `RETURN`
|
||||
erreicht) propagiert unverändert zu `evaluateCell`s bestehendem catch-all in
|
||||
`workbook.ts` → `#ERR` – dieselbe Absicherung, die schon jeden anderen
|
||||
Auswertungsfehler abfängt, keine neue Fehlerbehandlung nötig.
|
||||
|
||||
**Kein Endlos-Risiko trotz echter Schleife:** `MAX_LOOP_STEPS` (10.000,
|
||||
Summe aller Iterationen einer `evaluateBlockCall`-Ausführung, auch über
|
||||
verschachtelte Schleifen hinweg) wirft bei Überschreitung. Eine Kette
|
||||
verschachtelter `kind: 'block'`-Aufrufe bleibt zusätzlich durch
|
||||
`MAX_EXPANSION_DEPTH` (32, unverändert, greift beim Auflösen verschachtelter
|
||||
Aufrufe in `expandBlock`) begrenzt, sodass die Gesamtarbeit pro Formel immer
|
||||
endlich bleibt.
|
||||
|
||||
**Abhängigkeiten:** `collectRefs()` (`parser.ts`) bekommt einen `blockcall`-
|
||||
Fall, der sowohl `.args` als auch – über die neue Hilfsfunktion
|
||||
`collectRefsInBlock` – jeden im Block erreichbaren Zellbezug einsammelt,
|
||||
rekursiv durch `FOR`/`IF`. Das ist bewusst **konservativ**: ein Bezug in
|
||||
einem zur Laufzeit nicht genommenen `IF`-Zweig zählt trotzdem als
|
||||
Abhängigkeit (genau wie bei einem gewöhnlichen `IF` in Excel) – lieber
|
||||
gelegentlich unnötig neu rechnen als je einen veralteten Wert zeigen.
|
||||
|
||||
**Namens-Validierung ist Store-Sache, nicht Engine-Sache** (wie beim
|
||||
Fehlermodell durchgängig: die Engine liefert Mechanik, deutsche
|
||||
Fehlertexte entstehen im Store). `userFunctions.ts` exportiert dafür
|
||||
`declaredBlockNames(block)` (jeder eingeführte Name, inkl. Wiederholungen
|
||||
bei Wiederverwendung – z. B. das Akkumulator-Muster `ergebnis = 1 FOR x IN
|
||||
bereich DO ergebnis = ergebnis * x`, wo „ergebnis" zweimal auftaucht, aber
|
||||
**keine** Kollision ist) und `findInvalidForSource(block)`. `document.ts`s
|
||||
`validateCustomFunction` prüft damit: jeder Name gültig/nicht zellartig/kein
|
||||
reserviertes Wort (`FOR`/`IN`/`DO`/`RETURN`), kein *unterschiedlicher*
|
||||
lokaler Name überschattet einen Parameter (Wiederverwendung *desselben*
|
||||
Namens über mehrere Anweisungen ist dagegen erlaubt), und die
|
||||
Zyklus-Prüfung nutzt `collectCalledFunctionNamesInBlock(block)` (läuft das
|
||||
bestehende `collectCalledFunctionNames()` über jeden Blatt-Ausdruck des
|
||||
Blocks – kein zweiter AST-Walker nötig).
|
||||
Fehlertexte entstehen im Store). `userFunctions.ts` exportiert dafür,
|
||||
jeweils rekursiv durch verschachtelte `FOR`/`IF`-Körper:
|
||||
|
||||
- `declaredBlockNames(block)` – jeder eingeführte Name, inkl. Wiederholungen
|
||||
bei Wiederverwendung (z. B. das Akkumulator-Muster `ergebnis = 1 FOR x IN
|
||||
bereich DO ergebnis = ergebnis * x END`, wo „ergebnis" zweimal auftaucht,
|
||||
aber **keine** Kollision ist).
|
||||
- `findInvalidForSource(block)` – die erste `FOR`-Quelle, die syntaktisch
|
||||
weder Parameter noch Zelle/Bereich ist (egal wie tief verschachtelt).
|
||||
- `collectCalledFunctionNamesInBlock(block)` – läuft das bestehende
|
||||
`collectCalledFunctionNames()` über jeden Blatt-Ausdruck des Blocks; von
|
||||
der Store-Zyklusprüfung genutzt.
|
||||
- `blockAlwaysReturns(block)` – konservative statische Prüfung "erreicht
|
||||
jeder Pfad garantiert ein `RETURN`": wahr, wenn der Block eine
|
||||
`return`-Anweisung enthält, oder ein `IF` mit `ELSE`, bei dem **beide**
|
||||
Zweige immer returnen. Eine `FOR`-Schleife allein reicht nie (könnte 0
|
||||
Iterationen haben) – das ist eine Frühwarnung beim Speichern, die
|
||||
Laufzeit-Prüfung oben ("… endet ohne RETURN" → `#ERR`) ist die tatsächliche
|
||||
Absicherung.
|
||||
|
||||
`document.ts`s `validateCustomFunction` prüft damit: jeder Name gültig/nicht
|
||||
zellartig/kein reserviertes Wort (`FOR`/`IN`/`DO`/`END`/`IF`/`THEN`/`ELSE`/
|
||||
`RETURN`), kein *unterschiedlicher* lokaler Name überschattet einen
|
||||
Parameter (Wiederverwendung *desselben* Namens über mehrere Anweisungen ist
|
||||
dagegen erlaubt), `blockAlwaysReturns` sowie die Zyklus-Prüfung.
|
||||
|
||||
## Auswertungsmodell
|
||||
|
||||
- **Arithmetik ist numerisch.** Leere Zelle (`null`) zählt als `0`. Eine
|
||||
zahlenförmige Zeichenkette wird gecastet, jede andere ergibt `#VALUE!`.
|
||||
- **Vergleiche sind ebenfalls numerisch** (`toNumber()`-Koerzierung wie bei
|
||||
`+ - * /`), Ergebnis `1` (wahr) oder `0` (falsch); genutzt vor allem in
|
||||
einer `IF`-Bedingung eines „Mit Schleife"-Funktionskörpers (s. o.), aber in
|
||||
jeder Formel gültig.
|
||||
- **Fehler propagieren unverändert** durch Operatoren und Funktionen (der erste
|
||||
gefundene Fehler gewinnt).
|
||||
- Ein **Bereich** darf nur als Funktionsargument stehen. In Skalar-Position
|
||||
@@ -257,7 +321,7 @@ rufen `rebuild()` für einen vollständigen – trotzdem billigen – Durchlauf.
|
||||
| `#REF` | ungültiger/gelöschter Bezug, unbekannter Tabellen-/Blattname, **oder** ein unqualifizierter Tabellenname, der auf mehreren Blättern existiert und auch über die "gleiches Blatt wie die Formel"-Regel nicht eindeutig wird |
|
||||
| `#DIV/0!` | Division durch 0 (auch `AVG` ohne Zahlen) |
|
||||
| `#NAME?` | unbekannte Funktion (eingebaut oder benutzerdefiniert) |
|
||||
| `#ERR` | Parse-Fehler (`=A1+`, `=SUM(`); auch Arity-Mismatch/Expansionstiefen-Limit bei einer benutzerdefinierten Funktion, oder bei `kind: 'block'` eine ungültige FOR-Quelle bzw. mehr als `MAX_UNROLL_STEPS` abgerollte Zellen |
|
||||
| `#ERR` | Parse-Fehler (`=A1+`, `=SUM(`); auch Arity-Mismatch/Expansionstiefen-Limit bei einer benutzerdefinierten Funktion, oder bei `kind: 'block'` eine ungültige FOR-Quelle, mehr als `MAX_LOOP_STEPS` Schleifenschritte, oder eine Ausführung ohne je erreichtes `RETURN` |
|
||||
| `#CIRC` | Zirkelbezug – auch tabellen-/blattübergreifend |
|
||||
| `#VALUE!` | Typfehler in Arithmetik (Text × Zahl, Bereich als Skalar). *Erweiterung über die Aufgabenstellung hinaus, dokumentiert.* |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user