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:
@@ -32,6 +32,7 @@ import {
|
||||
collectCalledFunctionNamesInBlock,
|
||||
declaredBlockNames,
|
||||
findInvalidForSource,
|
||||
blockAlwaysReturns,
|
||||
type CellValue,
|
||||
type TableModel,
|
||||
type Token,
|
||||
@@ -787,6 +788,13 @@ function createStore() {
|
||||
if (badFor) {
|
||||
return `FOR ${badFor.varName} IN ...: die Quelle muss ein Parameter, eine Zelle oder ein Bereich sein, kein berechneter Ausdruck.`;
|
||||
}
|
||||
// conservative static check (see blockAlwaysReturns's own doc comment)
|
||||
// -- a real "no RETURN reached" (e.g. an empty FOR range) still
|
||||
// degrades to #ERR at the affected cell regardless, this just catches
|
||||
// the common case immediately, before ever saving.
|
||||
if (!blockAlwaysReturns(block)) {
|
||||
return 'Nicht jeder Codepfad endet mit RETURN.';
|
||||
}
|
||||
ownCalls = collectCalledFunctionNamesInBlock(block);
|
||||
} else {
|
||||
let ast;
|
||||
|
||||
Reference in New Issue
Block a user