@rsconcept/domain 1.6.1 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/{analyzer-camCAEkQ.d.ts → analyzer-QxWs73gP.d.ts} +2 -2
  2. package/dist/{ast-annotations-DiXmFraa.d.ts → ast-annotations-CtF7C9uK.d.ts} +2 -2
  3. package/dist/{calculator-BuVooqpb.d.ts → calculator-REeX2ZS1.d.ts} +2 -2
  4. package/dist/{error-CZXO5Y6a.d.ts → error-toW0TUHF.d.ts} +10 -1
  5. package/dist/index.d.ts +4 -4
  6. package/dist/library/rsengine.d.ts +1 -1
  7. package/dist/library/rsform-api.d.ts +1 -1
  8. package/dist/library/rsform.d.ts +1 -1
  9. package/dist/rslang/ast-annotations.d.ts +1 -1
  10. package/dist/rslang/error.d.ts +1 -1
  11. package/dist/rslang/error.js +9 -0
  12. package/dist/rslang/error.js.map +1 -1
  13. package/dist/rslang/eval/calculator.d.ts +1 -1
  14. package/dist/rslang/eval/evaluator.d.ts +1 -1
  15. package/dist/rslang/index.d.ts +4 -4
  16. package/dist/rslang/labels.d.ts +1 -1
  17. package/dist/rslang/labels.js +1 -1
  18. package/dist/rslang/labels.js.map +1 -1
  19. package/dist/rslang/parser/normalize.js +5 -1
  20. package/dist/rslang/parser/normalize.js.map +1 -1
  21. package/dist/rslang/parser/syntax-errors/index.d.ts +22 -0
  22. package/dist/rslang/parser/syntax-errors/index.js +2 -0
  23. package/dist/rslang/semantic/analyzer.d.ts +1 -1
  24. package/dist/rslang/semantic/analyzer.js +1 -1
  25. package/dist/rslang/semantic/type-auditor.d.ts +1 -1
  26. package/dist/rslang/semantic/value-auditor.d.ts +1 -1
  27. package/dist/syntax-errors-pCI6RicF.js +689 -0
  28. package/dist/syntax-errors-pCI6RicF.js.map +1 -0
  29. package/package.json +1 -1
  30. package/src/rslang/error.ts +9 -0
  31. package/src/rslang/labels.ts +1 -1
  32. package/src/rslang/parser/README.md +73 -0
  33. package/src/rslang/parser/normalize.ts +5 -1
  34. package/src/rslang/parser/parser.test.ts +92 -6
  35. package/src/rslang/parser/syntax-errors/README.md +61 -0
  36. package/src/rslang/parser/syntax-errors/ast-utils.ts +112 -0
  37. package/src/rslang/parser/syntax-errors/classify.ts +70 -0
  38. package/src/rslang/parser/syntax-errors/context.ts +25 -0
  39. package/src/rslang/parser/syntax-errors/error-builders.ts +28 -0
  40. package/src/rslang/parser/syntax-errors/expression-errors.ts +237 -0
  41. package/src/rslang/parser/syntax-errors/function-calls.ts +76 -0
  42. package/src/rslang/parser/syntax-errors/generators.ts +307 -0
  43. package/src/rslang/parser/syntax-errors/incomplete-formal.ts +76 -0
  44. package/src/rslang/parser/syntax-errors/incomplete-operands.ts +184 -0
  45. package/src/rslang/parser/syntax-errors/index.ts +102 -0
  46. package/src/rslang/parser/syntax-errors/trailing-slot.ts +45 -0
  47. package/dist/rslang/parser/syntax-errors.d.ts +0 -14
  48. package/dist/rslang/parser/syntax-errors.js +0 -311
  49. package/dist/rslang/parser/syntax-errors.js.map +0 -1
  50. package/src/rslang/parser/syntax-errors.ts +0 -511
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Types for the parse-error classifier pipeline.
3
+ */
4
+
5
+ import type { AstNode } from '../../../parsing';
6
+ import type { RSErrorDescription } from '../../error';
7
+ import type { TypeClass } from '../../semantic/typification';
8
+
9
+ /** Shared state passed through parse-error classifiers. */
10
+ export interface ClassifyContext {
11
+ hasBracketErrors: boolean;
12
+ expected?: TypeClass;
13
+ }
14
+
15
+ /** Classifies a single ERROR node into a specific syntax error, if possible. */
16
+ export type ErrorClassifier = (
17
+ node: AstNode,
18
+ expression: string,
19
+ context: ClassifyContext
20
+ ) => RSErrorDescription | null;
21
+
22
+ /** Skips classifiers that conflict with bracket-level diagnostics. */
23
+ export function whenNoBracketErrors(classifier: ErrorClassifier): ErrorClassifier {
24
+ return (node, expression, context) => (context.hasBracketErrors ? null : classifier(node, expression, context));
25
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Small helpers for building `RSErrorDescription` values from AST nodes.
3
+ */
4
+
5
+ import type { AstNode } from '../../../parsing';
6
+ import { type RSErrorCode, type RSErrorDescription } from '../../error';
7
+
8
+ export function errorAt(node: AstNode, code: RSErrorCode, params?: readonly string[]): RSErrorDescription {
9
+ return {
10
+ code,
11
+ from: node.from,
12
+ to: node.to,
13
+ params
14
+ };
15
+ }
16
+
17
+ export function hasOnlyClosingSuffix(expression: string, from: number): boolean {
18
+ const suffix = expression.slice(from).trim();
19
+ return suffix.length === 0 || /^[\]\)\}]*$/.test(suffix);
20
+ }
21
+
22
+ export function sliceText(node: AstNode, expression: string): string {
23
+ return expression.slice(node.from, node.to);
24
+ }
25
+
26
+ export function isTextToken(node: AstNode, expression: string, text: string): boolean {
27
+ return sliceText(node, expression) === text;
28
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Expression-level diagnostics (no AST required) and error deduplication.
3
+ */
4
+
5
+ import { RSErrorCode, type RSErrorDescription } from '../../error';
6
+
7
+ type OpenBracket = '(' | '[' | '{';
8
+ type CloseBracket = ')' | ']' | '}';
9
+
10
+ interface BracketFrame {
11
+ bracket: OpenBracket;
12
+ index: number;
13
+ }
14
+
15
+ const ALLOWED_EXPRESSION_CHARS = buildAllowedExpressionChars();
16
+
17
+ function buildAllowedExpressionChars(): Set<string> {
18
+ const chars = new Set<string>();
19
+ for (const chunk of [
20
+ '0123456789',
21
+ 'abcdefghijklmnopqrstuvwxyz',
22
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
23
+ '_',
24
+ '∅¬∀∃⇔⇒∨&ℬ+-*∪\\∆∩×∈∉⊆⊄⊂<>≥≤≠=,:;|[]{}()'
25
+ ]) {
26
+ for (const symbol of chunk) {
27
+ chars.add(symbol);
28
+ }
29
+ }
30
+ for (let codePoint = 0x03b1; codePoint <= 0x03c9; codePoint++) {
31
+ chars.add(String.fromCodePoint(codePoint));
32
+ }
33
+ return chars;
34
+ }
35
+
36
+ function isAllowedExpressionChar(symbol: string): boolean {
37
+ return /\s/u.test(symbol) || ALLOWED_EXPRESSION_CHARS.has(symbol);
38
+ }
39
+
40
+ export function extractForbiddenCharacterErrors(expression: string): RSErrorDescription[] {
41
+ const errors: RSErrorDescription[] = [];
42
+ let pos = 0;
43
+
44
+ while (pos < expression.length) {
45
+ const symbol = expression[pos];
46
+ if (isAllowedExpressionChar(symbol)) {
47
+ pos++;
48
+ continue;
49
+ }
50
+
51
+ const start = pos;
52
+ while (pos < expression.length && !isAllowedExpressionChar(expression[pos])) {
53
+ pos++;
54
+ }
55
+
56
+ errors.push({
57
+ code: RSErrorCode.forbiddenCharacter,
58
+ from: start,
59
+ to: pos,
60
+ params: [expression.slice(start, pos)]
61
+ });
62
+ }
63
+
64
+ return errors;
65
+ }
66
+
67
+ export function extractBracketErrors(expression: string): RSErrorDescription | null {
68
+ const stack: BracketFrame[] = [];
69
+
70
+ for (let pos = 0; pos < expression.length; pos++) {
71
+ const symbol = expression[pos];
72
+ if (isOpenBracket(symbol)) {
73
+ if (isDoubleParenthesis(expression, pos)) {
74
+ return {
75
+ code: RSErrorCode.doubleParenthesis,
76
+ from: pos,
77
+ to: pos + 2
78
+ };
79
+ }
80
+ stack.push({ bracket: symbol, index: pos });
81
+ continue;
82
+ }
83
+ if (!isCloseBracket(symbol)) {
84
+ continue;
85
+ }
86
+
87
+ const expectedOpen = closeToOpen(symbol);
88
+ const top = stack[stack.length - 1];
89
+ if (top === undefined) {
90
+ return {
91
+ code: RSErrorCode.missingOpenBracket,
92
+ from: pos,
93
+ to: pos + 1,
94
+ params: [symbol]
95
+ };
96
+ }
97
+
98
+ if (top.bracket !== expectedOpen) {
99
+ return {
100
+ code: RSErrorCode.bracketMismatch,
101
+ from: pos,
102
+ to: pos + 1,
103
+ params: [openToClose(top.bracket), symbol]
104
+ };
105
+ }
106
+
107
+ stack.pop();
108
+ }
109
+
110
+ if (stack.length > 0) {
111
+ const unclosed = stack[0];
112
+ return {
113
+ code: RSErrorCode.missingCloseBracket,
114
+ from: unclosed.index,
115
+ to: unclosed.index + 1,
116
+ params: [unclosed.bracket]
117
+ };
118
+ }
119
+
120
+ return null;
121
+ }
122
+
123
+ export function deduplicateErrors(errors: RSErrorDescription[]): RSErrorDescription[] {
124
+ const result: RSErrorDescription[] = [];
125
+
126
+ for (const error of errors) {
127
+ const duplicate = result.some(
128
+ existing => existing.code === error.code && existing.from === error.from && existing.to === error.to
129
+ );
130
+ if (duplicate) {
131
+ continue;
132
+ }
133
+
134
+ if (error.code !== RSErrorCode.unknownSyntax) {
135
+ for (let index = result.length - 1; index >= 0; index--) {
136
+ if (result[index].code === RSErrorCode.unknownSyntax && rangesOverlap(error, result[index])) {
137
+ result.splice(index, 1);
138
+ }
139
+ }
140
+ }
141
+
142
+ result.push(error);
143
+ }
144
+
145
+ const specific = result.filter(error => error.code !== RSErrorCode.unknownSyntax);
146
+ const filtered = result.filter(
147
+ error => error.code !== RSErrorCode.unknownSyntax || !specific.some(item => rangesOverlap(error, item))
148
+ );
149
+ return collapseContiguousUnknownSyntax(filtered);
150
+ }
151
+
152
+ function collapseContiguousUnknownSyntax(errors: RSErrorDescription[]): RSErrorDescription[] {
153
+ const unknowns = errors.filter(error => error.code === RSErrorCode.unknownSyntax);
154
+ if (unknowns.length <= 1) {
155
+ return errors;
156
+ }
157
+
158
+ const others = errors.filter(error => error.code !== RSErrorCode.unknownSyntax);
159
+ const first = [...unknowns].sort((left, right) => left.from - right.from)[0];
160
+ return [...others, first];
161
+ }
162
+
163
+ function rangesOverlap(a: RSErrorDescription, b: RSErrorDescription): boolean {
164
+ return a.from < b.to && b.from < a.to;
165
+ }
166
+
167
+ function isOpenBracket(symbol: string): symbol is OpenBracket {
168
+ return symbol === '(' || symbol === '[' || symbol === '{';
169
+ }
170
+
171
+ function isCloseBracket(symbol: string): symbol is CloseBracket {
172
+ return symbol === ')' || symbol === ']' || symbol === '}';
173
+ }
174
+
175
+ function openToClose(symbol: OpenBracket): CloseBracket {
176
+ switch (symbol) {
177
+ case '(':
178
+ return ')';
179
+ case '[':
180
+ return ']';
181
+ case '{':
182
+ return '}';
183
+ }
184
+ }
185
+
186
+ function closeToOpen(symbol: CloseBracket): OpenBracket {
187
+ switch (symbol) {
188
+ case ')':
189
+ return '(';
190
+ case ']':
191
+ return '[';
192
+ case '}':
193
+ return '{';
194
+ }
195
+ }
196
+
197
+ function isDoubleParenthesis(expression: string, pos: number): boolean {
198
+ const isOpenDoubleParenthesis = expression[pos] === '(' && expression[pos + 1] === '(';
199
+ if (!isOpenDoubleParenthesis) {
200
+ return false;
201
+ }
202
+
203
+ const outerClose = findMatchingCloseParen(expression, pos);
204
+ const innerClose = findMatchingCloseParen(expression, pos + 1);
205
+ if (outerClose < 0 || innerClose < 0) {
206
+ return false;
207
+ }
208
+
209
+ return outerClose === innerClose + 1;
210
+ }
211
+
212
+ function findMatchingCloseParen(expression: string, openPos: number): number {
213
+ if (expression[openPos] !== '(') {
214
+ return -1;
215
+ }
216
+
217
+ const stack: OpenBracket[] = [];
218
+ for (let index = openPos; index < expression.length; index++) {
219
+ const symbol = expression[index];
220
+ if (isOpenBracket(symbol)) {
221
+ stack.push(symbol);
222
+ continue;
223
+ }
224
+ if (!isCloseBracket(symbol)) {
225
+ continue;
226
+ }
227
+ const top = stack.pop();
228
+ if (top === undefined || top !== closeToOpen(symbol)) {
229
+ return -1;
230
+ }
231
+ if (stack.length === 0) {
232
+ return index;
233
+ }
234
+ }
235
+
236
+ return -1;
237
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Malformed calls: `Fi…(…)`, `F1` / `P1` without or with wrong argument brackets.
3
+ */
4
+
5
+ import type { AstNode } from '../../../parsing';
6
+ import { RSErrorCode, type RSErrorDescription } from '../../error';
7
+
8
+ import {
9
+ findAncestor,
10
+ findMatchingCloseBracket,
11
+ findRelatedNode,
12
+ isFilterNode,
13
+ isFunctionNode,
14
+ isPredicateNode
15
+ } from './ast-utils';
16
+ import type { ErrorClassifier } from './context';
17
+
18
+ export const classifyFilterParenMismatch: ErrorClassifier = (node, expression) => {
19
+ const filterNode = findAncestor(node, isFilterNode) ?? findRelatedNode(node, isFilterNode);
20
+ if (filterNode === null || expression[filterNode.to] !== '(') {
21
+ return null;
22
+ }
23
+
24
+ return {
25
+ code: RSErrorCode.invalidFilterSyntax,
26
+ from: filterNode.from,
27
+ to: filterNode.to + 1
28
+ };
29
+ };
30
+
31
+ export const classifyGlobalFuncCall: ErrorClassifier = (node, expression) => {
32
+ const funcNode =
33
+ findAncestor(node, n => isFunctionNode(n) || isPredicateNode(n)) ??
34
+ findRelatedNode(node, n => isFunctionNode(n) || isPredicateNode(n));
35
+ if (funcNode === null) {
36
+ return null;
37
+ }
38
+ return classifyFuncWithoutArgs(funcNode, expression);
39
+ };
40
+
41
+ function classifyFuncWithoutArgs(funcNode: AstNode, expression: string): RSErrorDescription | null {
42
+ const name = expression.slice(funcNode.from, funcNode.to);
43
+ const rest = expression.slice(funcNode.to);
44
+
45
+ const withoutArgs: RSErrorDescription = {
46
+ code: RSErrorCode.globalFuncWithoutArgs,
47
+ from: funcNode.from,
48
+ to: funcNode.to,
49
+ params: [name]
50
+ };
51
+
52
+ if (/^\s*$/.test(rest)) {
53
+ return withoutArgs;
54
+ }
55
+
56
+ if (/^\s*\(/.test(rest)) {
57
+ return {
58
+ code: RSErrorCode.globalFuncParenCall,
59
+ from: funcNode.from,
60
+ to: funcNode.to,
61
+ params: [name]
62
+ };
63
+ }
64
+
65
+ const openBracket = /^\s*\[/.exec(rest);
66
+ if (openBracket === null) {
67
+ return null;
68
+ }
69
+
70
+ const closeIdx = findMatchingCloseBracket(rest, openBracket[0].length - 1);
71
+ if (closeIdx >= 0 && rest.slice(openBracket[0].length, closeIdx).trim().length === 0) {
72
+ return withoutArgs;
73
+ }
74
+
75
+ return null;
76
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Incomplete generator bodies: declarative `D{…}`, imperative `I{…}`, recursive `R{…}`.
3
+ */
4
+
5
+ import type { AstNode } from '../../../parsing';
6
+ import { RSErrorCode, type RSErrorDescription } from '../../error';
7
+ import { Declarative, Imp_blocks, Imperative, Recursion, Setexpr } from '../parser.terms';
8
+ import { TokenID } from '../token';
9
+
10
+ import { findAncestor, isDescendantOf, isErrorPlaceholder, subtreeHasError } from './ast-utils';
11
+ import type { ErrorClassifier } from './context';
12
+ import { errorAt, isTextToken } from './error-builders';
13
+
14
+ export const classifyIncompleteGenerator: ErrorClassifier = (node, expression) =>
15
+ detectIncompleteDeclarative(node, expression) ??
16
+ detectIncompleteImperative(node, expression) ??
17
+ detectIncompleteRecursion(node, expression);
18
+
19
+ interface GeneratorBodySlot {
20
+ nodes: AstNode[];
21
+ headerEndIndex: number;
22
+ }
23
+
24
+ function detectIncompleteDeclarative(node: AstNode, expression: string): RSErrorDescription | null {
25
+ const generatorNode = findAncestor(
26
+ node,
27
+ target => target.typeID === Declarative || target.typeID === TokenID.NT_DECLARATIVE_EXPR
28
+ );
29
+ return detectIncompleteGeneratorBody(node, expression, generatorNode, RSErrorCode.expectedDeclarativeBody, slot =>
30
+ isIncompleteDeclarativeBody(slot.nodes)
31
+ );
32
+ }
33
+
34
+ function detectIncompleteImperative(node: AstNode, expression: string): RSErrorDescription | null {
35
+ const generatorNode = findAncestor(
36
+ node,
37
+ target => target.typeID === Imperative || target.typeID === TokenID.NT_IMPERATIVE_EXPR
38
+ );
39
+ return detectIncompleteGeneratorBody(node, expression, generatorNode, RSErrorCode.expectedImperativeBody, slot =>
40
+ isIncompleteImperativeBody(slot, expression)
41
+ );
42
+ }
43
+
44
+ function detectIncompleteRecursion(node: AstNode, expression: string): RSErrorDescription | null {
45
+ const generatorNode = findAncestor(node, target => target.typeID === Recursion);
46
+ return detectIncompleteGeneratorBody(node, expression, generatorNode, RSErrorCode.expectedRecursiveBody, slot =>
47
+ isIncompleteRecursionBody(slot.nodes)
48
+ );
49
+ }
50
+
51
+ function detectIncompleteGeneratorBody(
52
+ node: AstNode,
53
+ expression: string,
54
+ generatorNode: AstNode | null,
55
+ code: RSErrorCode,
56
+ isIncomplete: (slot: GeneratorBodySlot) => boolean
57
+ ): RSErrorDescription | null {
58
+ if (generatorNode === null || expression.slice(generatorNode.to).trim().length > 0) {
59
+ return null;
60
+ }
61
+
62
+ const slot = getGeneratorBodySlot(generatorNode, expression);
63
+ if (slot === null || !isIncomplete(slot) || !generatorHeaderIsValid(generatorNode, expression, slot.headerEndIndex)) {
64
+ return null;
65
+ }
66
+
67
+ if (!isErrorInBodySlot(node, slot, generatorNode)) {
68
+ return null;
69
+ }
70
+
71
+ return errorAt(node, code);
72
+ }
73
+
74
+ function getGeneratorBodySlot(generatorNode: AstNode, expression: string): GeneratorBodySlot | null {
75
+ if (generatorNode.typeID === Declarative || generatorNode.typeID === TokenID.NT_DECLARATIVE_EXPR) {
76
+ return getDeclarativeBodySlot(generatorNode, expression);
77
+ }
78
+ if (generatorNode.typeID === Imperative || generatorNode.typeID === TokenID.NT_IMPERATIVE_EXPR) {
79
+ return getImperativeBodySlot(generatorNode, expression);
80
+ }
81
+ if (generatorNode.typeID === Recursion) {
82
+ return getRecursionBodySlot(generatorNode, expression);
83
+ }
84
+ return null;
85
+ }
86
+
87
+ function getDeclarativeBodySlot(declNode: AstNode, expression: string): GeneratorBodySlot | null {
88
+ const pipeIndex = declNode.children.findIndex(child => isTextToken(child, expression, '|'));
89
+ if (pipeIndex >= 0) {
90
+ return {
91
+ nodes: contentChildren(declNode, expression, pipeIndex + 1),
92
+ headerEndIndex: pipeIndex + 1
93
+ };
94
+ }
95
+
96
+ const domainNode = findDeclarativeDomain(declNode, expression);
97
+ if (domainNode === null) {
98
+ return null;
99
+ }
100
+
101
+ const domainIndex = declNode.children.indexOf(domainNode);
102
+ return {
103
+ nodes: contentChildren(declNode, expression, domainIndex + 1),
104
+ headerEndIndex: domainIndex + 1
105
+ };
106
+ }
107
+
108
+ function getImperativeBodySlot(impNode: AstNode, expression: string): GeneratorBodySlot | null {
109
+ const pipeIndex = impNode.children.findIndex(child => isTextToken(child, expression, '|'));
110
+ if (pipeIndex >= 0) {
111
+ return {
112
+ nodes: contentChildren(impNode, expression, pipeIndex + 1),
113
+ headerEndIndex: pipeIndex + 1
114
+ };
115
+ }
116
+
117
+ const tupleNode = findImperativeTuple(impNode);
118
+ if (tupleNode === null) {
119
+ return null;
120
+ }
121
+
122
+ const tupleIndex = impNode.children.indexOf(tupleNode);
123
+ return {
124
+ nodes: contentChildren(impNode, expression, tupleIndex + 1),
125
+ headerEndIndex: tupleIndex + 1
126
+ };
127
+ }
128
+
129
+ function getRecursionBodySlot(recNode: AstNode, expression: string): GeneratorBodySlot | null {
130
+ const initNode = findRecursionInit(recNode, expression);
131
+ if (initNode === null) {
132
+ return null;
133
+ }
134
+
135
+ const initIndex = recNode.children.indexOf(initNode);
136
+ const afterInit = contentChildren(recNode, expression, initIndex + 1);
137
+ const lastPipeIndex = findLastPipeIndex(afterInit, expression);
138
+ if (lastPipeIndex < 0) {
139
+ return {
140
+ nodes: afterInit,
141
+ headerEndIndex: initIndex + 1
142
+ };
143
+ }
144
+
145
+ return {
146
+ nodes: afterInit.slice(lastPipeIndex + 1),
147
+ headerEndIndex: recNode.children.indexOf(afterInit[lastPipeIndex]) + 1
148
+ };
149
+ }
150
+
151
+ function contentChildren(generatorNode: AstNode, expression: string, fromIndex: number): AstNode[] {
152
+ return generatorNode.children.slice(fromIndex).filter(child => !isClosingBrace(child, expression));
153
+ }
154
+
155
+ function findDeclarativeDomain(declNode: AstNode, expression: string): AstNode | null {
156
+ const inIndex = declNode.children.findIndex(child => isTextToken(child, expression, '∈'));
157
+ if (inIndex < 0) {
158
+ return null;
159
+ }
160
+
161
+ for (let index = inIndex + 1; index < declNode.children.length; index++) {
162
+ const child = declNode.children[index];
163
+ if (isTextToken(child, expression, '|') || isClosingBrace(child, expression)) {
164
+ break;
165
+ }
166
+ if (child.typeID === Setexpr) {
167
+ return child;
168
+ }
169
+ }
170
+
171
+ return null;
172
+ }
173
+
174
+ function findImperativeTuple(impNode: AstNode): AstNode | null {
175
+ for (const child of impNode.children) {
176
+ if (child.typeID === Setexpr) {
177
+ return child;
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+
183
+ function findLastPipeIndex(nodes: AstNode[], expression: string): number {
184
+ for (let index = nodes.length - 1; index >= 0; index--) {
185
+ if (isTextToken(nodes[index], expression, '|')) {
186
+ return index;
187
+ }
188
+ }
189
+ return -1;
190
+ }
191
+
192
+ function generatorHeaderIsValid(generatorNode: AstNode, expression: string, headerEndIndex: number): boolean {
193
+ for (let index = 0; index < headerEndIndex; index++) {
194
+ const child = generatorNode.children[index];
195
+ if (child.typeID === TokenID.ERROR) {
196
+ return false;
197
+ }
198
+ if (isStructuralGeneratorToken(child, expression)) {
199
+ continue;
200
+ }
201
+ if (subtreeHasError(child)) {
202
+ return false;
203
+ }
204
+ }
205
+ return true;
206
+ }
207
+
208
+ function isIncompleteDeclarativeBody(nodes: AstNode[]): boolean {
209
+ return isEmptyOrErrorOnlyBody(nodes);
210
+ }
211
+
212
+ function isIncompleteImperativeBody(slot: GeneratorBodySlot, expression: string): boolean {
213
+ if (slot.nodes.length === 0) {
214
+ return true;
215
+ }
216
+ if (slot.nodes.length === 1 && slot.nodes[0].typeID === Imp_blocks) {
217
+ return isImpBlocksIncomplete(slot.nodes[0], expression);
218
+ }
219
+ return isEmptyOrErrorOnlyBody(slot.nodes);
220
+ }
221
+
222
+ function isIncompleteRecursionBody(nodes: AstNode[]): boolean {
223
+ return isEmptyOrErrorOnlyBody(nodes);
224
+ }
225
+
226
+ function isEmptyOrErrorOnlyBody(nodes: AstNode[]): boolean {
227
+ if (nodes.length === 0) {
228
+ return true;
229
+ }
230
+ if (nodes.length === 1) {
231
+ return isErrorPlaceholder(nodes[0]);
232
+ }
233
+ return false;
234
+ }
235
+
236
+ function isImpBlocksIncomplete(blocksNode: AstNode, expression: string): boolean {
237
+ if (blocksNode.children.length === 0) {
238
+ return true;
239
+ }
240
+
241
+ const lastChild = blocksNode.children[blocksNode.children.length - 1];
242
+ if (lastChild.typeID === TokenID.ERROR) {
243
+ return true;
244
+ }
245
+ if (isTextToken(lastChild, expression, ';')) {
246
+ return true;
247
+ }
248
+ if (lastChild.typeID === Setexpr && isErrorPlaceholder(lastChild)) {
249
+ return true;
250
+ }
251
+
252
+ return false;
253
+ }
254
+
255
+ function isErrorInBodySlot(errorNode: AstNode, slot: GeneratorBodySlot, generatorNode: AstNode): boolean {
256
+ if (slot.nodes.length === 0) {
257
+ const bodyStart = generatorNode.children[slot.headerEndIndex]?.from ?? generatorNode.from;
258
+ return errorNode.from >= bodyStart;
259
+ }
260
+
261
+ for (const bodyNode of slot.nodes) {
262
+ if (errorNode === bodyNode || isDescendantOf(bodyNode, errorNode)) {
263
+ if (isErrorPlaceholder(bodyNode)) {
264
+ return true;
265
+ }
266
+ if (bodyNode.typeID === Imp_blocks && isImpBlocksTailError(bodyNode, errorNode)) {
267
+ return true;
268
+ }
269
+ }
270
+ }
271
+
272
+ return false;
273
+ }
274
+
275
+ function isImpBlocksTailError(blocksNode: AstNode, errorNode: AstNode): boolean {
276
+ const lastChild = blocksNode.children[blocksNode.children.length - 1];
277
+ return lastChild.typeID === TokenID.ERROR && (errorNode === lastChild || isDescendantOf(lastChild, errorNode));
278
+ }
279
+
280
+ function findRecursionInit(recursionNode: AstNode, expression: string): AstNode | null {
281
+ const assignIndex = recursionNode.children.findIndex(child => isTextToken(child, expression, ':='));
282
+ if (assignIndex < 0) {
283
+ return null;
284
+ }
285
+
286
+ for (let index = assignIndex + 1; index < recursionNode.children.length; index++) {
287
+ const child = recursionNode.children[index];
288
+ if (child.typeID === TokenID.ERROR) {
289
+ break;
290
+ }
291
+ if (isTextToken(child, expression, '|') || isClosingBrace(child, expression)) {
292
+ continue;
293
+ }
294
+ return child;
295
+ }
296
+
297
+ return null;
298
+ }
299
+
300
+ function isClosingBrace(node: AstNode, expression: string): boolean {
301
+ return node.typeID !== TokenID.ERROR && isTextToken(node, expression, '}');
302
+ }
303
+
304
+ function isStructuralGeneratorToken(node: AstNode, expression: string): boolean {
305
+ const text = expression.slice(node.from, node.to);
306
+ return text === '{' || text === '}' || text === '|' || text === '∈' || text === ':=' || text === ';';
307
+ }