lny-interpreter 0.99.0
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.
Potentially problematic release.
This version of lny-interpreter might be problematic. Click here for more details.
- package/.vscode/launch.json +19 -0
- package/README.md +96 -0
- package/book_ALT/LennySmartIcon1.png +0 -0
- package/book_ALT/chapter01.md +141 -0
- package/book_ALT/chapter02.md +48 -0
- package/book_ALT/chapter03.md +28 -0
- package/book_ALT/chapter04.md +19 -0
- package/book_ALT/chapter05.md +21 -0
- package/book_ALT/chapter06.md +167 -0
- package/book_ALT/chapter07.md +232 -0
- package/book_ALT/chapter08.md +200 -0
- package/book_ALT/chapter09.md +175 -0
- package/book_ALT/chapter10.md +196 -0
- package/book_ALT/deckblatt.md +60 -0
- package/book_ALT/images/Lenny01.png +0 -0
- package/book_ALT/images/Lenny02.png +0 -0
- package/book_ALT/images/Lenny03.png +0 -0
- package/book_ALT/images/Lenny04.png +0 -0
- package/book_ALT/images/Lenny05.png +0 -0
- package/book_ALT/images/Lenny06.png +0 -0
- package/book_ALT/images/Lenny07.png +0 -0
- package/book_ALT/images/Lenny08.png +0 -0
- package/book_ALT/images/Lenny09.png +0 -0
- package/book_ALT/images/Lenny10.png +0 -0
- package/book_ALT/images/Lenny11.png +0 -0
- package/book_ALT/images/Lenny12.png +0 -0
- package/book_ALT/images/Lenny13.png +0 -0
- package/book_ALT/images/Lenny14.png +0 -0
- package/book_ALT/images/Lenny15.png +0 -0
- package/book_ALT/lehrerheft.md +255 -0
- package/dist/ast/ast.d.ts +126 -0
- package/dist/ast/ast.d.ts.map +1 -0
- package/dist/ast/ast.js +3 -0
- package/dist/ast/ast.js.map +1 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +1067 -0
- package/dist/browser.js.map +1 -0
- package/dist/examples.d.ts +11 -0
- package/dist/examples.d.ts.map +1 -0
- package/dist/examples.js +65 -0
- package/dist/examples.js.map +1 -0
- package/dist/i18n/i18n.d.ts +18 -0
- package/dist/i18n/i18n.d.ts.map +1 -0
- package/dist/i18n/i18n.js +505 -0
- package/dist/i18n/i18n.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/interpreter/interpreter.d.ts +40 -0
- package/dist/interpreter/interpreter.d.ts.map +1 -0
- package/dist/interpreter/interpreter.js +415 -0
- package/dist/interpreter/interpreter.js.map +1 -0
- package/dist/lexer/lexer.d.ts +87 -0
- package/dist/lexer/lexer.d.ts.map +1 -0
- package/dist/lexer/lexer.js +425 -0
- package/dist/lexer/lexer.js.map +1 -0
- package/dist/lny-config.yml +6 -0
- package/dist/parser/parser.d.ts +50 -0
- package/dist/parser/parser.d.ts.map +1 -0
- package/dist/parser/parser.js +605 -0
- package/dist/parser/parser.js.map +1 -0
- package/dist/repl.d.ts +3 -0
- package/dist/repl.d.ts.map +1 -0
- package/dist/repl.js +472 -0
- package/dist/repl.js.map +1 -0
- package/dist/run-file.d.ts +2 -0
- package/dist/run-file.d.ts.map +1 -0
- package/dist/run-file.js +140 -0
- package/dist/run-file.js.map +1 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +72 -0
- package/dist/test.js.map +1 -0
- package/dist/world/world.d.ts +103 -0
- package/dist/world/world.d.ts.map +1 -0
- package/dist/world/world.js +416 -0
- package/dist/world/world.js.map +1 -0
- package/docs/About.md +19 -0
- package/docs/INTERPRETER.md +240 -0
- package/docs/README.md +68 -0
- package/docs/i18n/README.md +20 -0
- package/docs/lny-referenz.md +161 -0
- package/docs/lny_ueberblick.md +33 -0
- package/examples/00-demo.lny +3 -0
- package/examples/01-variable-assignment.lny +4 -0
- package/examples/02-conditional.lny +7 -0
- package/examples/03-while-loop.lny +6 -0
- package/examples/04-for-loop.lny +5 -0
- package/examples/05-lenny-movement.lny +10 -0
- package/examples/06-procedure.lny +6 -0
- package/examples/fuer-range-1.lny +4 -0
- package/examples/fuer-range-2.lny +6 -0
- package/examples/fuer-range-3.lny +6 -0
- package/examples/fuer-range-4.lny +4 -0
- package/examples/fuer-range-5.lny +6 -0
- package/examples/solange-verschachtelt.lny +12 -0
- package/favicon.ico +0 -0
- package/index.html +108 -0
- package/jest.config.js +7 -0
- package/lny-config.yml +6 -0
- package/package.json +37 -0
- package/settings.json +4 -0
- package/src/ast/ast.ts +182 -0
- package/src/browser.ts +1274 -0
- package/src/examples.ts +68 -0
- package/src/i18n/i18n.ts +537 -0
- package/src/index.ts +6 -0
- package/src/interpreter/interpreter.ts +453 -0
- package/src/lexer/lexer.ts +493 -0
- package/src/parser/parser.ts +711 -0
- package/src/repl.ts +496 -0
- package/src/test.ts +71 -0
- package/src/world/world.ts +512 -0
- package/style.css +315 -0
- package/test1.lny +2 -0
- package/tests/interpreter.test.ts +42 -0
- package/tests/parser.test.ts +18 -0
- package/tsconfig.json +19 -0
- package/vercel.json +7 -0
- package/vscode-lny/.vscode/launch.json +13 -0
- package/vscode-lny/.vscodeignore +3 -0
- package/vscode-lny/README.md +83 -0
- package/vscode-lny/language-configuration.json +19 -0
- package/vscode-lny/license.txt +21 -0
- package/vscode-lny/lny-language-0.1.0.vsix +0 -0
- package/vscode-lny/package.json +41 -0
- package/vscode-lny/snippets/lny.json +136 -0
- package/vscode-lny/syntaxes/lny.tmLanguage.json +85 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import { Token, TokenType, Lexer } from '../lexer/lexer';
|
|
2
|
+
import * as AST from '../ast/ast';
|
|
3
|
+
import { Language, I18n } from '../i18n/i18n';
|
|
4
|
+
|
|
5
|
+
export class Parser {
|
|
6
|
+
private tokens: Token[];
|
|
7
|
+
private current: number = 0;
|
|
8
|
+
private i18n: I18n;
|
|
9
|
+
|
|
10
|
+
constructor(source: string, language: Language = 'de') {
|
|
11
|
+
this.i18n = new I18n(language);
|
|
12
|
+
const lexer = new Lexer(source, language);
|
|
13
|
+
this.tokens = lexer.tokenize();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private syntax(message: string): Error {
|
|
17
|
+
const token = this.getErrorToken();
|
|
18
|
+
if (token) {
|
|
19
|
+
return new Error(
|
|
20
|
+
`${this.i18n.t('error_syntax')}: ${message} (line ${token.line}, column ${token.column})`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return new Error(`${this.i18n.t('error_syntax')}: ${message}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private getErrorToken(): Token | undefined {
|
|
27
|
+
if (this.current >= 0 && this.current < this.tokens.length) {
|
|
28
|
+
return this.tokens[this.current];
|
|
29
|
+
}
|
|
30
|
+
if (this.current > 0 && this.current - 1 < this.tokens.length) {
|
|
31
|
+
return this.tokens[this.current - 1];
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
parse(): AST.Program {
|
|
37
|
+
const statements: AST.Statement[] = [];
|
|
38
|
+
while (!this.isAtEnd()) {
|
|
39
|
+
const stmt = this.parseStatement();
|
|
40
|
+
if (stmt) {
|
|
41
|
+
statements.push(stmt);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
type: 'Program',
|
|
46
|
+
body: statements,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private parseStatement(): AST.Statement | null {
|
|
51
|
+
// Comments
|
|
52
|
+
if (this.match(TokenType.REM)) {
|
|
53
|
+
return this.parseComment();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Variable declaration
|
|
57
|
+
if (this.match(TokenType.VAR)) {
|
|
58
|
+
return this.parseVariableDeclaration();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Explicit variable assignment with SET keyword
|
|
62
|
+
if (this.match(TokenType.SET)) {
|
|
63
|
+
return this.parseSetAssignment();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// Variable assignment
|
|
69
|
+
if (this.check(TokenType.IDENTIFIER)) {
|
|
70
|
+
const checkpoint = this.current;
|
|
71
|
+
const identifier = this.advance();
|
|
72
|
+
|
|
73
|
+
if (this.isAssignmentOperator(this.peek().type)) {
|
|
74
|
+
return this.parseAssignmentFromIdentifier(identifier.value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// If not assignment, reset and parse as expression statement
|
|
78
|
+
this.current = checkpoint;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Control Flow
|
|
82
|
+
if (this.match(TokenType.IF)) {
|
|
83
|
+
return this.parseIfStatement();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (this.match(TokenType.WHILE)) {
|
|
87
|
+
return this.parseWhileStatement();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (this.match(TokenType.FOR)) {
|
|
91
|
+
return this.parseForStatement();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Procedure declaration
|
|
95
|
+
if (this.match(TokenType.PROC)) {
|
|
96
|
+
return this.parseProcedureDeclaration();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Log statement
|
|
100
|
+
if (this.match(TokenType.LOG)) {
|
|
101
|
+
return this.parseLogStatement();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Return statement
|
|
105
|
+
if (this.match(TokenType.RETURN)) {
|
|
106
|
+
return this.parseReturnStatement();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Expression statement
|
|
110
|
+
if (!this.isAtEnd()) {
|
|
111
|
+
return {
|
|
112
|
+
type: 'ExpressionStatement',
|
|
113
|
+
expression: this.parseExpression(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private parseComment(): AST.CommentStatement {
|
|
121
|
+
// Das aktuelle Token ist das Kommentar-Token (REM), also einfach übernehmen
|
|
122
|
+
const token = this.peek();
|
|
123
|
+
const text = token.value;
|
|
124
|
+
this.advance(); // Zum nächsten Token weitergehen
|
|
125
|
+
return {
|
|
126
|
+
type: 'CommentStatement',
|
|
127
|
+
text: text.trim(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private parseVariableDeclaration(): AST.VariableDeclaration {
|
|
132
|
+
const varToken = this.previous();
|
|
133
|
+
const name = this.expectIdentifier('Expected variable name after VAR');
|
|
134
|
+
let initialValue: AST.Expression | undefined;
|
|
135
|
+
|
|
136
|
+
if (this.match(TokenType.ASSIGN)) {
|
|
137
|
+
initialValue = this.parseExpression();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
type: 'VariableDeclaration',
|
|
142
|
+
name,
|
|
143
|
+
initialValue,
|
|
144
|
+
line: varToken.line,
|
|
145
|
+
column: varToken.column
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private parseSetAssignment(): AST.VariableAssignment {
|
|
150
|
+
const setToken = this.previous();
|
|
151
|
+
const name = this.expectIdentifier('Expected variable name after SETZE');
|
|
152
|
+
if (!this.match(TokenType.AUF)) {
|
|
153
|
+
throw this.syntax('Expected AUF after variable name in SETZE statement');
|
|
154
|
+
}
|
|
155
|
+
const value = this.parseExpression();
|
|
156
|
+
return {
|
|
157
|
+
type: 'VariableAssignment',
|
|
158
|
+
name,
|
|
159
|
+
operator: 'AUF',
|
|
160
|
+
value,
|
|
161
|
+
line: setToken.line,
|
|
162
|
+
column: setToken.column
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private parseAssignmentFromIdentifier(name: string): AST.VariableAssignment {
|
|
167
|
+
const nameToken = this.previous();
|
|
168
|
+
if (!this.isAssignmentOperator(this.peek().type)) {
|
|
169
|
+
throw this.syntax('Expected assignment operator (<<, +=, -=, *=, /=)');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const operator = this.advance().value;
|
|
173
|
+
const value = this.parseExpression();
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
type: 'VariableAssignment',
|
|
177
|
+
name,
|
|
178
|
+
operator,
|
|
179
|
+
value,
|
|
180
|
+
line: nameToken.line,
|
|
181
|
+
column: nameToken.column
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private parseIfStatement(): AST.IfStatement {
|
|
186
|
+
const condition = this.parseExpression();
|
|
187
|
+
|
|
188
|
+
if (!this.match(TokenType.THEN)) {
|
|
189
|
+
throw this.syntax('Expected THEN after IF condition');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const consequent: AST.Statement[] = [];
|
|
193
|
+
while (!this.check(TokenType.ELSE) && !this.check(TokenType.END) && !this.isAtEnd()) {
|
|
194
|
+
const stmt = this.parseStatement();
|
|
195
|
+
if (stmt) consequent.push(stmt);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let alternate: AST.Statement[] | undefined;
|
|
199
|
+
|
|
200
|
+
if (this.match(TokenType.ELSE)) {
|
|
201
|
+
alternate = [];
|
|
202
|
+
while (!this.check(TokenType.END) && !this.isAtEnd()) {
|
|
203
|
+
const stmt = this.parseStatement();
|
|
204
|
+
if (stmt) alternate.push(stmt);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Allow EOF as implicit END (for REPL)
|
|
209
|
+
if (!this.match(TokenType.END)) {
|
|
210
|
+
if (!this.isAtEnd()) {
|
|
211
|
+
throw this.syntax('Expected END after IF statement');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
type: 'IfStatement',
|
|
217
|
+
condition,
|
|
218
|
+
consequent,
|
|
219
|
+
alternate,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private parseWhileStatement(): AST.WhileStatement {
|
|
224
|
+
const condition = this.parseExpression();
|
|
225
|
+
|
|
226
|
+
const body: AST.Statement[] = [];
|
|
227
|
+
while (!this.check(TokenType.END) && !this.isAtEnd()) {
|
|
228
|
+
const stmt = this.parseStatement();
|
|
229
|
+
if (stmt) body.push(stmt);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Allow EOF as implicit END (for REPL)
|
|
233
|
+
if (!this.match(TokenType.END)) {
|
|
234
|
+
if (!this.isAtEnd()) {
|
|
235
|
+
throw this.syntax('Expected END after WHILE body');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
type: 'WhileStatement',
|
|
241
|
+
condition,
|
|
242
|
+
body,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private parseForStatement(): AST.ForStatement {
|
|
247
|
+
const variable = this.expectIdentifier('Expected variable name after FOR');
|
|
248
|
+
|
|
249
|
+
if (!this.match(TokenType.IN)) {
|
|
250
|
+
throw this.syntax('Expected IN after FOR variable');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const iterable = this.parseExpression();
|
|
254
|
+
|
|
255
|
+
const body: AST.Statement[] = [];
|
|
256
|
+
while (!this.check(TokenType.END) && !this.isAtEnd()) {
|
|
257
|
+
const stmt = this.parseStatement();
|
|
258
|
+
if (stmt) body.push(stmt);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Allow EOF as implicit END (for REPL)
|
|
262
|
+
if (!this.match(TokenType.END)) {
|
|
263
|
+
if (!this.isAtEnd()) {
|
|
264
|
+
throw this.syntax('Expected END after FOR body');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
type: 'ForStatement',
|
|
270
|
+
variable,
|
|
271
|
+
iterable,
|
|
272
|
+
body,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private parseProcedureDeclaration(): AST.ProcedureDeclaration {
|
|
277
|
+
const name = this.expectIdentifier('Expected procedure name');
|
|
278
|
+
const parameters: string[] = [];
|
|
279
|
+
|
|
280
|
+
// Parse parameters if they exist
|
|
281
|
+
if (this.match(TokenType.LPAREN)) {
|
|
282
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
283
|
+
do {
|
|
284
|
+
parameters.push(this.expectIdentifier('Expected parameter name'));
|
|
285
|
+
} while (this.match(TokenType.COMMA));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!this.match(TokenType.RPAREN)) {
|
|
289
|
+
throw this.syntax('Expected ) after parameters');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const body: AST.Statement[] = [];
|
|
294
|
+
while (!this.check(TokenType.END) && !this.isAtEnd()) {
|
|
295
|
+
const stmt = this.parseStatement();
|
|
296
|
+
if (stmt) body.push(stmt);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!this.match(TokenType.END)) {
|
|
300
|
+
throw this.syntax('Expected END after PROC body');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
type: 'ProcedureDeclaration',
|
|
305
|
+
name,
|
|
306
|
+
parameters,
|
|
307
|
+
body,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private parseLogStatement(): AST.LogStatement {
|
|
312
|
+
const expression = this.parseExpression();
|
|
313
|
+
return {
|
|
314
|
+
type: 'LogStatement',
|
|
315
|
+
expression,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private parseReturnStatement(): AST.ReturnStatement {
|
|
320
|
+
let argument: AST.Expression | undefined;
|
|
321
|
+
|
|
322
|
+
if (!this.isAtEnd() && !this.check(TokenType.END) && !this.check(TokenType.ELSE)) {
|
|
323
|
+
argument = this.parseExpression();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
type: 'ReturnStatement',
|
|
328
|
+
argument,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private parseExpression(): AST.Expression {
|
|
333
|
+
return this.parseOrExpression();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// --- List and Range Parsing ---
|
|
337
|
+
private parseListLiteral(): AST.Expression {
|
|
338
|
+
this.expect(TokenType.LBRACKET, 'Expected [');
|
|
339
|
+
// Check for range: [start..end]
|
|
340
|
+
const first = this.parseExpression();
|
|
341
|
+
if (this.match(TokenType.DOT) && this.match(TokenType.DOT)) {
|
|
342
|
+
// [start..end]
|
|
343
|
+
const end = this.parseExpression();
|
|
344
|
+
this.expect(TokenType.RBRACKET, 'Expected ] after range');
|
|
345
|
+
return {
|
|
346
|
+
type: 'RangeLiteral',
|
|
347
|
+
start: first,
|
|
348
|
+
end: end,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
// Otherwise, normal list
|
|
352
|
+
const elements = [first];
|
|
353
|
+
while (this.match(TokenType.COMMA)) {
|
|
354
|
+
elements.push(this.parseExpression());
|
|
355
|
+
}
|
|
356
|
+
this.expect(TokenType.RBRACKET, 'Expected ] after list');
|
|
357
|
+
return {
|
|
358
|
+
type: 'ListLiteral',
|
|
359
|
+
elements,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private parseOrExpression(): AST.Expression {
|
|
364
|
+
let expr = this.parseAndExpression();
|
|
365
|
+
|
|
366
|
+
while (this.match(TokenType.OR)) {
|
|
367
|
+
const operator = this.previous().value;
|
|
368
|
+
const right = this.parseAndExpression();
|
|
369
|
+
expr = {
|
|
370
|
+
type: 'BinaryExpression',
|
|
371
|
+
left: expr,
|
|
372
|
+
operator,
|
|
373
|
+
right,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return expr;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private parseAndExpression(): AST.Expression {
|
|
381
|
+
let expr = this.parseEqualityExpression();
|
|
382
|
+
|
|
383
|
+
while (this.match(TokenType.AND)) {
|
|
384
|
+
const operator = this.previous().value;
|
|
385
|
+
const right = this.parseEqualityExpression();
|
|
386
|
+
expr = {
|
|
387
|
+
type: 'BinaryExpression',
|
|
388
|
+
left: expr,
|
|
389
|
+
operator,
|
|
390
|
+
right,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return expr;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private parseEqualityExpression(): AST.Expression {
|
|
398
|
+
let expr = this.parseRelationalExpression();
|
|
399
|
+
|
|
400
|
+
while (this.match(TokenType.EQUAL, TokenType.NOT_EQUAL)) {
|
|
401
|
+
const operator = this.previous().value;
|
|
402
|
+
const right = this.parseRelationalExpression();
|
|
403
|
+
expr = {
|
|
404
|
+
type: 'BinaryExpression',
|
|
405
|
+
left: expr,
|
|
406
|
+
operator,
|
|
407
|
+
right,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return expr;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private parseRelationalExpression(): AST.Expression {
|
|
415
|
+
let expr = this.parseAdditiveExpression();
|
|
416
|
+
|
|
417
|
+
while (this.match(TokenType.LESS, TokenType.GREATER, TokenType.LESS_EQUAL, TokenType.GREATER_EQUAL)) {
|
|
418
|
+
const operator = this.previous().value;
|
|
419
|
+
const right = this.parseAdditiveExpression();
|
|
420
|
+
expr = {
|
|
421
|
+
type: 'BinaryExpression',
|
|
422
|
+
left: expr,
|
|
423
|
+
operator,
|
|
424
|
+
right,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return expr;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private parseAdditiveExpression(): AST.Expression {
|
|
432
|
+
let expr = this.parseMultiplicativeExpression();
|
|
433
|
+
|
|
434
|
+
while (this.match(TokenType.PLUS, TokenType.MINUS)) {
|
|
435
|
+
const operator = this.previous().value;
|
|
436
|
+
const right = this.parseMultiplicativeExpression();
|
|
437
|
+
expr = {
|
|
438
|
+
type: 'BinaryExpression',
|
|
439
|
+
left: expr,
|
|
440
|
+
operator,
|
|
441
|
+
right,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return expr;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private parseMultiplicativeExpression(): AST.Expression {
|
|
449
|
+
let expr = this.parseExponentiationExpression();
|
|
450
|
+
|
|
451
|
+
while (this.match(TokenType.MULTIPLY, TokenType.DIVIDE, TokenType.MODULO)) {
|
|
452
|
+
const operator = this.previous().value;
|
|
453
|
+
const right = this.parseExponentiationExpression();
|
|
454
|
+
expr = {
|
|
455
|
+
type: 'BinaryExpression',
|
|
456
|
+
left: expr,
|
|
457
|
+
operator,
|
|
458
|
+
right,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return expr;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private parseExponentiationExpression(): AST.Expression {
|
|
466
|
+
let expr = this.parseUnaryExpression();
|
|
467
|
+
|
|
468
|
+
if (this.match(TokenType.POWER)) {
|
|
469
|
+
const operator = this.previous().value;
|
|
470
|
+
const right = this.parseExponentiationExpression(); // Right associative
|
|
471
|
+
expr = {
|
|
472
|
+
type: 'BinaryExpression',
|
|
473
|
+
left: expr,
|
|
474
|
+
operator,
|
|
475
|
+
right,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return expr;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private parseUnaryExpression(): AST.Expression {
|
|
483
|
+
if (this.match(TokenType.NOT, TokenType.MINUS)) {
|
|
484
|
+
const operator = this.previous().value;
|
|
485
|
+
const argument = this.parseUnaryExpression();
|
|
486
|
+
return {
|
|
487
|
+
type: 'UnaryExpression',
|
|
488
|
+
operator,
|
|
489
|
+
argument,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return this.parsePostfixExpression();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private parsePostfixExpression(): AST.Expression {
|
|
497
|
+
let expr = this.parsePrimaryExpression();
|
|
498
|
+
|
|
499
|
+
while (true) {
|
|
500
|
+
if (this.match(TokenType.LPAREN)) {
|
|
501
|
+
// Function call
|
|
502
|
+
if (expr.type !== 'Identifier') {
|
|
503
|
+
throw this.syntax('Only identifiers can be called');
|
|
504
|
+
}
|
|
505
|
+
const args: AST.Expression[] = [];
|
|
506
|
+
|
|
507
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
508
|
+
do {
|
|
509
|
+
args.push(this.parseExpression());
|
|
510
|
+
} while (this.match(TokenType.COMMA));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (!this.match(TokenType.RPAREN)) {
|
|
514
|
+
throw this.syntax('Expected ) after function arguments');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
expr = {
|
|
518
|
+
type: 'CallExpression',
|
|
519
|
+
callee: expr as AST.Identifier,
|
|
520
|
+
arguments: args,
|
|
521
|
+
};
|
|
522
|
+
} else {
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return expr;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private parsePrimaryExpression(): AST.Expression {
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
// Numbers
|
|
534
|
+
if (this.check(TokenType.NUMBER)) {
|
|
535
|
+
const value = parseFloat(this.advance().value);
|
|
536
|
+
return {
|
|
537
|
+
type: 'NumberLiteral',
|
|
538
|
+
value,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Strings
|
|
543
|
+
if (this.check(TokenType.STRING)) {
|
|
544
|
+
const value = this.advance().value;
|
|
545
|
+
return {
|
|
546
|
+
type: 'StringLiteral',
|
|
547
|
+
value,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Booleans
|
|
552
|
+
if (this.match(TokenType.TRUE)) {
|
|
553
|
+
return {
|
|
554
|
+
type: 'BooleanLiteral',
|
|
555
|
+
value: true,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (this.match(TokenType.FALSE)) {
|
|
560
|
+
return {
|
|
561
|
+
type: 'BooleanLiteral',
|
|
562
|
+
value: false,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Nothing
|
|
567
|
+
if (this.match(TokenType.NOTHING)) {
|
|
568
|
+
return {
|
|
569
|
+
type: 'NothingLiteral',
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Lists (inkl. Bereichsoperator)
|
|
574
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
575
|
+
return this.parseListLiteral();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Parenthesized expressions
|
|
579
|
+
if (this.match(TokenType.LPAREN)) {
|
|
580
|
+
const expression = this.parseExpression();
|
|
581
|
+
|
|
582
|
+
if (!this.match(TokenType.RPAREN)) {
|
|
583
|
+
throw this.syntax('Expected ) after expression');
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
type: 'ParenthesizedExpression',
|
|
588
|
+
expression,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Private attribute access
|
|
593
|
+
if (this.match(TokenType.PRIVATE)) {
|
|
594
|
+
const name = this.expectIdentifier('Expected attribute name after PRIVATE');
|
|
595
|
+
return {
|
|
596
|
+
type: 'PrivateAttribute',
|
|
597
|
+
name,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Global attribute access
|
|
602
|
+
if (this.match(TokenType.GLOBAL)) {
|
|
603
|
+
const name = this.expectIdentifier('Expected attribute name after GLOBAL');
|
|
604
|
+
return {
|
|
605
|
+
type: 'GlobalAttribute',
|
|
606
|
+
name,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Identifiers
|
|
611
|
+
if (this.match(TokenType.IDENTIFIER)) {
|
|
612
|
+
const token = this.previous();
|
|
613
|
+
return {
|
|
614
|
+
type: 'Identifier',
|
|
615
|
+
name: token.value,
|
|
616
|
+
line: token.line,
|
|
617
|
+
column: token.column
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
throw this.syntax(`Unexpected token: ${this.peek().value}`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Helper methods
|
|
625
|
+
private expectIdentifier(expectedMessage: string): string {
|
|
626
|
+
const token = this.peek();
|
|
627
|
+
|
|
628
|
+
if (token.type === TokenType.IDENTIFIER) {
|
|
629
|
+
return this.advance().value;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (this.isReservedWordToken(token.type)) {
|
|
633
|
+
throw this.syntax(this.i18n.t('error_reserved_word', { name: token.value }));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
throw this.syntax(expectedMessage);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private isReservedWordToken(type: TokenType): boolean {
|
|
640
|
+
// Prüfe reservierte Wörter nur für die aktuelle Sprache
|
|
641
|
+
// y ist z.B. nur in Spanisch reserviert, nicht in Deutsch/Englisch
|
|
642
|
+
const reserved: TokenType[] = [
|
|
643
|
+
TokenType.IF, TokenType.THEN, TokenType.ELSE, TokenType.END, TokenType.WHILE, TokenType.FOR, TokenType.IN,
|
|
644
|
+
TokenType.VAR, TokenType.SET, TokenType.AUF, TokenType.LIST, TokenType.PROC, TokenType.RETURN, TokenType.LOG, TokenType.REM,
|
|
645
|
+
TokenType.NOT, TokenType.TRUE, TokenType.FALSE, TokenType.NOTHING, TokenType.PRIVATE, TokenType.GLOBAL
|
|
646
|
+
];
|
|
647
|
+
// AND und OR nur reserviert, wenn sie in der aktuellen Sprache wirklich so heißen
|
|
648
|
+
if (this.i18n.t('kw_and').toLowerCase() === 'and' && type === TokenType.AND) return true;
|
|
649
|
+
if (this.i18n.t('kw_or').toLowerCase() === 'or' && type === TokenType.OR) return true;
|
|
650
|
+
// In anderen Sprachen (z.B. Spanisch: 'y' für AND) ist AND nur reserviert, wenn das aktuelle Keyword so heißt
|
|
651
|
+
if (type === TokenType.AND && this.i18n.t('kw_and').toLowerCase() !== 'and') return false;
|
|
652
|
+
if (type === TokenType.OR && this.i18n.t('kw_or').toLowerCase() !== 'or') return false;
|
|
653
|
+
return reserved.includes(type);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private isAssignmentOperator(type: TokenType): boolean {
|
|
657
|
+
return (
|
|
658
|
+
type === TokenType.ASSIGN ||
|
|
659
|
+
type === TokenType.PLUS_ASSIGN ||
|
|
660
|
+
type === TokenType.MINUS_ASSIGN ||
|
|
661
|
+
type === TokenType.MULT_ASSIGN ||
|
|
662
|
+
type === TokenType.DIV_ASSIGN
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
private match(...types: TokenType[]): boolean {
|
|
667
|
+
for (const type of types) {
|
|
668
|
+
if (this.check(type)) {
|
|
669
|
+
this.advance();
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private check(type: TokenType): boolean {
|
|
677
|
+
if (this.isAtEnd()) return false;
|
|
678
|
+
return this.peek().type === type;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private advance(): Token {
|
|
682
|
+
if (!this.isAtEnd()) {
|
|
683
|
+
this.current++;
|
|
684
|
+
}
|
|
685
|
+
return this.previous();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private isAtEnd(): boolean {
|
|
689
|
+
return this.peek().type === TokenType.EOF;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
private peek(): Token {
|
|
693
|
+
return this.tokens[this.current];
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private previous(): Token {
|
|
697
|
+
return this.tokens[this.current - 1];
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Consumes the next token if it matches the expected type, otherwise throws a syntax error.
|
|
702
|
+
* @param type The expected token type
|
|
703
|
+
* @param message The error message if the token does not match
|
|
704
|
+
*/
|
|
705
|
+
private expect(type: TokenType, message: string): Token {
|
|
706
|
+
if (this.check(type)) {
|
|
707
|
+
return this.advance();
|
|
708
|
+
}
|
|
709
|
+
throw this.syntax(message);
|
|
710
|
+
}
|
|
711
|
+
}
|