hispano-lang 1.0.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.
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/bin/hispano.js +313 -0
- package/dist/cli.d.ts +46 -0
- package/dist/evaluator.js +1391 -0
- package/dist/index.d.ts +185 -0
- package/dist/index.js +74 -0
- package/dist/interpreter.js +73 -0
- package/dist/parser.js +1050 -0
- package/dist/tokenizer.js +445 -0
- package/package.json +72 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for HispanoLang
|
|
3
|
+
* Converts tokens into an Abstract Syntax Tree (AST)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class Parser {
|
|
7
|
+
constructor(tokens) {
|
|
8
|
+
this.tokens = tokens;
|
|
9
|
+
this.current = 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parses tokens and returns a list of statements
|
|
14
|
+
* @returns {Array} List of statements (AST)
|
|
15
|
+
*/
|
|
16
|
+
parse() {
|
|
17
|
+
const statements = [];
|
|
18
|
+
|
|
19
|
+
while (!this.isAtEnd()) {
|
|
20
|
+
statements.push(this.declaration());
|
|
21
|
+
|
|
22
|
+
// Consume semicolon if present between statements
|
|
23
|
+
if (this.match('SEMICOLON')) {
|
|
24
|
+
// Semicolon consumed
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return statements;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parses a declaration
|
|
33
|
+
* @returns {Object} Parsed declaration
|
|
34
|
+
*/
|
|
35
|
+
declaration() {
|
|
36
|
+
try {
|
|
37
|
+
if (this.match('VARIABLE')) {
|
|
38
|
+
return this.variableDeclaration();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (this.match('FUNCION')) {
|
|
42
|
+
return this.functionDeclaration();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (this.match('MOSTRAR')) {
|
|
46
|
+
return this.mostrarStatement();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (this.match('LEER')) {
|
|
50
|
+
return this.leerStatement();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (this.match('SI')) {
|
|
54
|
+
return this.ifStatement();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (this.match('MIENTRAS')) {
|
|
58
|
+
return this.whileStatement();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (this.match('PARA')) {
|
|
62
|
+
return this.forStatement();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (this.match('RETORNAR')) {
|
|
66
|
+
return this.returnStatement();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (this.match('ROMPER')) {
|
|
70
|
+
return this.breakStatement();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.match('CONTINUAR')) {
|
|
74
|
+
return this.continueStatement();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (this.match('INTENTAR')) {
|
|
78
|
+
return this.tryStatement();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return this.expressionStatement();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.synchronize();
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parses a variable declaration
|
|
90
|
+
* @returns {Object} Variable declaration
|
|
91
|
+
*/
|
|
92
|
+
variableDeclaration() {
|
|
93
|
+
let name;
|
|
94
|
+
if (this.match('IDENTIFIER')) {
|
|
95
|
+
name = this.previous();
|
|
96
|
+
} else if (this.match('AND')) {
|
|
97
|
+
name = this.previous();
|
|
98
|
+
} else {
|
|
99
|
+
throw new Error('Expected variable name');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let initializer = null;
|
|
103
|
+
if (this.match('EQUAL')) {
|
|
104
|
+
initializer = this.expression();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
type: 'VariableDeclaration',
|
|
109
|
+
name: name.lexeme,
|
|
110
|
+
initializer,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parses a function declaration
|
|
116
|
+
* @returns {Object} Function declaration
|
|
117
|
+
*/
|
|
118
|
+
functionDeclaration() {
|
|
119
|
+
const name = this.consume('IDENTIFIER', 'Expected function name');
|
|
120
|
+
this.consume('LEFT_PAREN', 'Expected ( after function name');
|
|
121
|
+
|
|
122
|
+
const parameters = [];
|
|
123
|
+
if (!this.check('RIGHT_PAREN')) {
|
|
124
|
+
do {
|
|
125
|
+
if (parameters.length >= 255) {
|
|
126
|
+
throw new Error('Cannot have more than 255 parameters');
|
|
127
|
+
}
|
|
128
|
+
let param;
|
|
129
|
+
if (this.match('IDENTIFIER')) {
|
|
130
|
+
param = this.previous();
|
|
131
|
+
} else if (this.match('AND')) {
|
|
132
|
+
param = this.previous();
|
|
133
|
+
} else {
|
|
134
|
+
throw new Error('Expected parameter name');
|
|
135
|
+
}
|
|
136
|
+
parameters.push(param.lexeme);
|
|
137
|
+
} while (this.match('COMMA'));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.consume('RIGHT_PAREN', 'Expected ) after parameters');
|
|
141
|
+
this.consume('LEFT_BRACE', 'Expected { before function body');
|
|
142
|
+
const body = this.block();
|
|
143
|
+
this.consume('RIGHT_BRACE', 'Expected } after function body');
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
type: 'FunctionDeclaration',
|
|
147
|
+
name: name.lexeme,
|
|
148
|
+
parameters,
|
|
149
|
+
body,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Parses a show statement
|
|
155
|
+
* @returns {Object} Show statement
|
|
156
|
+
*/
|
|
157
|
+
mostrarStatement() {
|
|
158
|
+
const value = this.expression();
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
type: 'MostrarStatement',
|
|
162
|
+
expression: value,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Parses a read statement
|
|
168
|
+
* @returns {Object} Read statement
|
|
169
|
+
*/
|
|
170
|
+
leerStatement() {
|
|
171
|
+
// Parse the variable name to store the input
|
|
172
|
+
const variableName = this.consume(
|
|
173
|
+
'IDENTIFIER',
|
|
174
|
+
'Expected variable name after leer'
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Optional prompt message
|
|
178
|
+
let prompt = null;
|
|
179
|
+
if (this.match('STRING')) {
|
|
180
|
+
prompt = this.previous().literal;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
type: 'LeerStatement',
|
|
185
|
+
variable: variableName.lexeme,
|
|
186
|
+
prompt: prompt,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Parses an if statement
|
|
192
|
+
* @returns {Object} If statement
|
|
193
|
+
*/
|
|
194
|
+
ifStatement() {
|
|
195
|
+
const condition = this.expression();
|
|
196
|
+
this.consume('LEFT_BRACE', 'Expected { after condition');
|
|
197
|
+
const thenBranch = this.block();
|
|
198
|
+
this.consume('RIGHT_BRACE', 'Expected } after block');
|
|
199
|
+
let elseBranch = null;
|
|
200
|
+
|
|
201
|
+
if (this.match('SINO')) {
|
|
202
|
+
this.consume('LEFT_BRACE', 'Expected { after else');
|
|
203
|
+
elseBranch = this.block();
|
|
204
|
+
this.consume('RIGHT_BRACE', 'Expected } after else block');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
type: 'IfStatement',
|
|
209
|
+
condition,
|
|
210
|
+
thenBranch,
|
|
211
|
+
elseBranch,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Parses a while statement
|
|
217
|
+
* @returns {Object} While statement
|
|
218
|
+
*/
|
|
219
|
+
whileStatement() {
|
|
220
|
+
const condition = this.expression();
|
|
221
|
+
this.consume('LEFT_BRACE', 'Expected { after condition');
|
|
222
|
+
const body = this.block();
|
|
223
|
+
this.consume('RIGHT_BRACE', 'Expected } after block');
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
type: 'WhileStatement',
|
|
227
|
+
condition,
|
|
228
|
+
body,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Parses a for statement
|
|
234
|
+
* @returns {Object} For statement
|
|
235
|
+
*/
|
|
236
|
+
forStatement() {
|
|
237
|
+
this.consume('LEFT_PAREN', 'Expected ( after para');
|
|
238
|
+
|
|
239
|
+
// Initializer (optional)
|
|
240
|
+
let initializer = null;
|
|
241
|
+
if (!this.check('SEMICOLON')) {
|
|
242
|
+
if (this.match('VARIABLE')) {
|
|
243
|
+
initializer = this.variableDeclaration();
|
|
244
|
+
} else {
|
|
245
|
+
initializer = this.expressionStatement();
|
|
246
|
+
}
|
|
247
|
+
// Consume the semicolon after initializer
|
|
248
|
+
this.consume('SEMICOLON', 'Expected ; after for initializer');
|
|
249
|
+
} else {
|
|
250
|
+
// Skip the first semicolon if no initializer
|
|
251
|
+
this.advance();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Condition (optional)
|
|
255
|
+
let condition = null;
|
|
256
|
+
if (!this.check('SEMICOLON')) {
|
|
257
|
+
condition = this.expression();
|
|
258
|
+
}
|
|
259
|
+
this.consume('SEMICOLON', 'Expected ; after condition');
|
|
260
|
+
|
|
261
|
+
// Increment (optional)
|
|
262
|
+
let increment = null;
|
|
263
|
+
if (!this.check('RIGHT_PAREN')) {
|
|
264
|
+
increment = this.expression();
|
|
265
|
+
}
|
|
266
|
+
this.consume('RIGHT_PAREN', 'Expected ) after for clause');
|
|
267
|
+
|
|
268
|
+
this.consume('LEFT_BRACE', 'Expected { after for clause');
|
|
269
|
+
const body = this.block();
|
|
270
|
+
this.consume('RIGHT_BRACE', 'Expected } after block');
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
type: 'ForStatement',
|
|
274
|
+
initializer,
|
|
275
|
+
condition,
|
|
276
|
+
increment,
|
|
277
|
+
body,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Parses a return statement
|
|
283
|
+
* @returns {Object} Return statement
|
|
284
|
+
*/
|
|
285
|
+
returnStatement() {
|
|
286
|
+
const keyword = this.previous();
|
|
287
|
+
let value = null;
|
|
288
|
+
if (!this.check('RIGHT_BRACE')) {
|
|
289
|
+
value = this.expression();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
type: 'ReturnStatement',
|
|
294
|
+
keyword,
|
|
295
|
+
value,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Parses a break statement
|
|
301
|
+
* @returns {Object} Break statement
|
|
302
|
+
*/
|
|
303
|
+
breakStatement() {
|
|
304
|
+
const keyword = this.previous();
|
|
305
|
+
return {
|
|
306
|
+
type: 'BreakStatement',
|
|
307
|
+
keyword,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Parses a continue statement
|
|
313
|
+
* @returns {Object} Continue statement
|
|
314
|
+
*/
|
|
315
|
+
continueStatement() {
|
|
316
|
+
const keyword = this.previous();
|
|
317
|
+
return {
|
|
318
|
+
type: 'ContinueStatement',
|
|
319
|
+
keyword,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Parses a code block
|
|
325
|
+
* @returns {Array} List of statements in the block
|
|
326
|
+
*/
|
|
327
|
+
block() {
|
|
328
|
+
const statements = [];
|
|
329
|
+
|
|
330
|
+
while (!this.check('RIGHT_BRACE') && !this.isAtEnd()) {
|
|
331
|
+
statements.push(this.declaration());
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return statements;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Parses an expression statement
|
|
339
|
+
* @returns {Object} Expression statement
|
|
340
|
+
*/
|
|
341
|
+
expressionStatement() {
|
|
342
|
+
const expr = this.expression();
|
|
343
|
+
return {
|
|
344
|
+
type: 'ExpressionStatement',
|
|
345
|
+
expression: expr,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Parses an expression
|
|
351
|
+
* @returns {Object} Parsed expression
|
|
352
|
+
*/
|
|
353
|
+
expression() {
|
|
354
|
+
return this.assignment();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Parses a complete expression that can be followed by array access
|
|
359
|
+
* @returns {Object} Parsed expression
|
|
360
|
+
*/
|
|
361
|
+
parseExpression() {
|
|
362
|
+
let expr = this.equality();
|
|
363
|
+
|
|
364
|
+
while (this.match('LEFT_BRACKET')) {
|
|
365
|
+
expr = this.finishArrayAccess(expr);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return expr;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Parses an assignment
|
|
373
|
+
* @returns {Object} Parsed assignment
|
|
374
|
+
*/
|
|
375
|
+
assignment() {
|
|
376
|
+
let expr = this.logicalOr();
|
|
377
|
+
|
|
378
|
+
if (this.match('EQUAL')) {
|
|
379
|
+
const equals = this.previous();
|
|
380
|
+
const value = this.logicalOr();
|
|
381
|
+
|
|
382
|
+
if (expr.type === 'Variable') {
|
|
383
|
+
const name = expr.name;
|
|
384
|
+
return {
|
|
385
|
+
type: 'Assign',
|
|
386
|
+
name,
|
|
387
|
+
value,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (expr.type === 'ArrayAccess') {
|
|
392
|
+
return {
|
|
393
|
+
type: 'ArrayAssign',
|
|
394
|
+
array: expr.array,
|
|
395
|
+
index: expr.index,
|
|
396
|
+
value,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (expr.type === 'PropertyAccess') {
|
|
401
|
+
return {
|
|
402
|
+
type: 'PropertyAssign',
|
|
403
|
+
object: expr.object,
|
|
404
|
+
name: expr.name,
|
|
405
|
+
value,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
throw new Error('Invalid assignment target');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Handle compound assignment operators
|
|
413
|
+
if (
|
|
414
|
+
this.match(
|
|
415
|
+
'PLUS_EQUAL',
|
|
416
|
+
'MINUS_EQUAL',
|
|
417
|
+
'STAR_EQUAL',
|
|
418
|
+
'SLASH_EQUAL',
|
|
419
|
+
'PERCENT_EQUAL'
|
|
420
|
+
)
|
|
421
|
+
) {
|
|
422
|
+
const operator = this.previous();
|
|
423
|
+
const value = this.logicalOr();
|
|
424
|
+
|
|
425
|
+
if (expr.type === 'Variable') {
|
|
426
|
+
const name = expr.name;
|
|
427
|
+
return {
|
|
428
|
+
type: 'CompoundAssign',
|
|
429
|
+
name,
|
|
430
|
+
operator: operator.type,
|
|
431
|
+
value,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (expr.type === 'ArrayAccess') {
|
|
436
|
+
return {
|
|
437
|
+
type: 'CompoundArrayAssign',
|
|
438
|
+
array: expr.array,
|
|
439
|
+
index: expr.index,
|
|
440
|
+
operator: operator.type,
|
|
441
|
+
value,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (expr.type === 'PropertyAccess') {
|
|
446
|
+
return {
|
|
447
|
+
type: 'CompoundPropertyAssign',
|
|
448
|
+
object: expr.object,
|
|
449
|
+
name: expr.name,
|
|
450
|
+
operator: operator.type,
|
|
451
|
+
value,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
throw new Error('Invalid compound assignment target');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return expr;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Parses an equality expression
|
|
463
|
+
* @returns {Object} Equality expression
|
|
464
|
+
*/
|
|
465
|
+
equality() {
|
|
466
|
+
let expr = this.comparison();
|
|
467
|
+
|
|
468
|
+
while (this.match('EQUAL_EQUAL', 'BANG_EQUAL')) {
|
|
469
|
+
const operator = this.previous();
|
|
470
|
+
const right = this.comparison();
|
|
471
|
+
expr = {
|
|
472
|
+
type: 'Binary',
|
|
473
|
+
left: expr,
|
|
474
|
+
operator: operator.type,
|
|
475
|
+
right,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return expr;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Parses a logical AND expression
|
|
484
|
+
* @returns {Object} Logical AND expression
|
|
485
|
+
*/
|
|
486
|
+
logicalAnd() {
|
|
487
|
+
let expr = this.equality();
|
|
488
|
+
|
|
489
|
+
while (this.match('AND')) {
|
|
490
|
+
const operator = this.previous();
|
|
491
|
+
const right = this.equality();
|
|
492
|
+
expr = {
|
|
493
|
+
type: 'Logical',
|
|
494
|
+
left: expr,
|
|
495
|
+
operator: operator.type,
|
|
496
|
+
right,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return expr;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Parses a logical OR expression
|
|
505
|
+
* @returns {Object} Logical OR expression
|
|
506
|
+
*/
|
|
507
|
+
logicalOr() {
|
|
508
|
+
let expr = this.logicalAnd();
|
|
509
|
+
|
|
510
|
+
while (this.match('OR')) {
|
|
511
|
+
const operator = this.previous();
|
|
512
|
+
const right = this.logicalAnd();
|
|
513
|
+
expr = {
|
|
514
|
+
type: 'Logical',
|
|
515
|
+
left: expr,
|
|
516
|
+
operator: operator.type,
|
|
517
|
+
right,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return expr;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Parses a comparison expression
|
|
526
|
+
* @returns {Object} Comparison expression
|
|
527
|
+
*/
|
|
528
|
+
comparison() {
|
|
529
|
+
let expr = this.term();
|
|
530
|
+
|
|
531
|
+
while (this.match('GREATER', 'GREATER_EQUAL', 'LESS', 'LESS_EQUAL')) {
|
|
532
|
+
const operator = this.previous();
|
|
533
|
+
const right = this.term();
|
|
534
|
+
expr = {
|
|
535
|
+
type: 'Binary',
|
|
536
|
+
left: expr,
|
|
537
|
+
operator: operator.type,
|
|
538
|
+
right,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return expr;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Parses a term expression
|
|
547
|
+
* @returns {Object} Term expression
|
|
548
|
+
*/
|
|
549
|
+
term() {
|
|
550
|
+
let expr = this.factor();
|
|
551
|
+
|
|
552
|
+
while (this.match('MINUS', 'PLUS')) {
|
|
553
|
+
const operator = this.previous();
|
|
554
|
+
const right = this.factor();
|
|
555
|
+
expr = {
|
|
556
|
+
type: 'Binary',
|
|
557
|
+
left: expr,
|
|
558
|
+
operator: operator.type,
|
|
559
|
+
right,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return expr;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Parses a factor expression
|
|
568
|
+
* @returns {Object} Factor expression
|
|
569
|
+
*/
|
|
570
|
+
factor() {
|
|
571
|
+
let expr = this.unary();
|
|
572
|
+
|
|
573
|
+
while (this.match('SLASH', 'STAR', 'PERCENT')) {
|
|
574
|
+
const operator = this.previous();
|
|
575
|
+
const right = this.unary();
|
|
576
|
+
expr = {
|
|
577
|
+
type: 'Binary',
|
|
578
|
+
left: expr,
|
|
579
|
+
operator: operator.type,
|
|
580
|
+
right,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return expr;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Parses a unary expression
|
|
589
|
+
* @returns {Object} Unary expression
|
|
590
|
+
*/
|
|
591
|
+
unary() {
|
|
592
|
+
if (this.match('BANG', 'MINUS')) {
|
|
593
|
+
const operator = this.previous();
|
|
594
|
+
const right = this.unary();
|
|
595
|
+
return {
|
|
596
|
+
type: 'Unary',
|
|
597
|
+
operator: operator.type,
|
|
598
|
+
right,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return this.postfix();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Parses a postfix expression (increment/decrement)
|
|
607
|
+
* @returns {Object} Postfix expression
|
|
608
|
+
*/
|
|
609
|
+
postfix() {
|
|
610
|
+
let expr = this.call();
|
|
611
|
+
|
|
612
|
+
while (this.match('PLUS_PLUS', 'MINUS_MINUS')) {
|
|
613
|
+
const operator = this.previous();
|
|
614
|
+
expr = {
|
|
615
|
+
type: 'Postfix',
|
|
616
|
+
operator: operator.type,
|
|
617
|
+
operand: expr,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return expr;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Parses a call expression (function calls and array access)
|
|
626
|
+
* @returns {Object} Call expression
|
|
627
|
+
*/
|
|
628
|
+
call() {
|
|
629
|
+
let expr = this.primary();
|
|
630
|
+
|
|
631
|
+
while (true) {
|
|
632
|
+
if (this.match('LEFT_BRACKET')) {
|
|
633
|
+
expr = this.finishArrayAccess(expr);
|
|
634
|
+
} else if (this.match('DOT')) {
|
|
635
|
+
expr = this.finishPropertyAccess(expr);
|
|
636
|
+
} else {
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return expr;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Parses a primary expression
|
|
646
|
+
* @returns {Object} Primary expression
|
|
647
|
+
*/
|
|
648
|
+
primary() {
|
|
649
|
+
if (this.match('FALSE')) return { type: 'Literal', value: false };
|
|
650
|
+
if (this.match('TRUE')) return { type: 'Literal', value: true };
|
|
651
|
+
if (this.match('NULL')) return { type: 'Literal', value: null };
|
|
652
|
+
if (this.match('UNDEFINED')) return { type: 'Literal', value: undefined };
|
|
653
|
+
if (this.match('NUMBER', 'STRING')) {
|
|
654
|
+
return {
|
|
655
|
+
type: 'Literal',
|
|
656
|
+
value: this.previous().literal,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (this.match('IDENTIFIER')) {
|
|
661
|
+
const identifier = this.previous();
|
|
662
|
+
if (this.check('LEFT_PAREN')) {
|
|
663
|
+
this.advance(); // Consume the LEFT_PAREN
|
|
664
|
+
return this.finishCall(identifier);
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
type: 'Variable',
|
|
668
|
+
name: identifier.lexeme,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (this.match('AND')) {
|
|
673
|
+
const identifier = this.previous();
|
|
674
|
+
if (this.check('LEFT_PAREN')) {
|
|
675
|
+
this.advance(); // Consume the LEFT_PAREN
|
|
676
|
+
return this.finishCall(identifier);
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
type: 'Variable',
|
|
680
|
+
name: identifier.lexeme,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (this.match('LEFT_PAREN')) {
|
|
685
|
+
const expr = this.expression();
|
|
686
|
+
this.consume('RIGHT_PAREN', 'Expected ) after expression');
|
|
687
|
+
return expr;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (this.match('LEFT_BRACKET')) {
|
|
691
|
+
return this.arrayLiteral();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (this.match('LEFT_BRACE')) {
|
|
695
|
+
return this.objectLiteral();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (this.match('FUNCION')) {
|
|
699
|
+
return this.anonymousFunction();
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
throw new Error('Expected expression');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Parses an anonymous function
|
|
707
|
+
* @returns {Object} Anonymous function expression
|
|
708
|
+
*/
|
|
709
|
+
anonymousFunction() {
|
|
710
|
+
this.consume('LEFT_PAREN', 'Expected ( after funcion');
|
|
711
|
+
|
|
712
|
+
const parameters = [];
|
|
713
|
+
if (!this.check('RIGHT_PAREN')) {
|
|
714
|
+
do {
|
|
715
|
+
if (parameters.length >= 255) {
|
|
716
|
+
throw new Error('Cannot have more than 255 parameters');
|
|
717
|
+
}
|
|
718
|
+
let param;
|
|
719
|
+
if (this.match('IDENTIFIER')) {
|
|
720
|
+
param = this.previous();
|
|
721
|
+
} else if (this.match('AND')) {
|
|
722
|
+
param = this.previous();
|
|
723
|
+
} else {
|
|
724
|
+
throw new Error('Expected parameter name');
|
|
725
|
+
}
|
|
726
|
+
parameters.push(param.lexeme);
|
|
727
|
+
} while (this.match('COMMA'));
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
this.consume('RIGHT_PAREN', 'Expected ) after parameters');
|
|
731
|
+
this.consume('LEFT_BRACE', 'Expected { before function body');
|
|
732
|
+
const body = this.block();
|
|
733
|
+
this.consume('RIGHT_BRACE', 'Expected } after function body');
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
type: 'AnonymousFunction',
|
|
737
|
+
parameters,
|
|
738
|
+
body,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Finishes parsing an array access
|
|
744
|
+
* @param {Object} array - The array being accessed
|
|
745
|
+
* @returns {Object} Array access expression
|
|
746
|
+
*/
|
|
747
|
+
finishArrayAccess(array) {
|
|
748
|
+
const index = this.expression();
|
|
749
|
+
this.consume('RIGHT_BRACKET', 'Expected ] after array index');
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
type: 'ArrayAccess',
|
|
753
|
+
array,
|
|
754
|
+
index,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Finishes parsing a property access
|
|
760
|
+
* @param {Object} object - The object being accessed
|
|
761
|
+
* @returns {Object} Property access expression
|
|
762
|
+
*/
|
|
763
|
+
finishPropertyAccess(object) {
|
|
764
|
+
this.consume('IDENTIFIER', 'Expected property name after .');
|
|
765
|
+
const name = this.previous();
|
|
766
|
+
|
|
767
|
+
// Check if this is a method call (array or string)
|
|
768
|
+
if (
|
|
769
|
+
name.lexeme === 'longitud' ||
|
|
770
|
+
name.lexeme === 'primero' ||
|
|
771
|
+
name.lexeme === 'ultimo' ||
|
|
772
|
+
name.lexeme === 'agregar' ||
|
|
773
|
+
name.lexeme === 'remover' ||
|
|
774
|
+
name.lexeme === 'contiene' ||
|
|
775
|
+
name.lexeme === 'recorrer' ||
|
|
776
|
+
name.lexeme === 'mayusculas' ||
|
|
777
|
+
name.lexeme === 'minusculas'
|
|
778
|
+
) {
|
|
779
|
+
// Check if there are parentheses (method call syntax)
|
|
780
|
+
if (this.match('LEFT_PAREN')) {
|
|
781
|
+
// Consume the opening parenthesis
|
|
782
|
+
// Check if there are arguments
|
|
783
|
+
if (!this.check('RIGHT_PAREN')) {
|
|
784
|
+
// Handle methods that accept arguments (like agregar, contiene, recorrer)
|
|
785
|
+
if (
|
|
786
|
+
name.lexeme === 'agregar' ||
|
|
787
|
+
name.lexeme === 'contiene' ||
|
|
788
|
+
name.lexeme === 'recorrer'
|
|
789
|
+
) {
|
|
790
|
+
const args = [];
|
|
791
|
+
do {
|
|
792
|
+
args.push(this.expression());
|
|
793
|
+
} while (this.match('COMMA'));
|
|
794
|
+
this.consume('RIGHT_PAREN', 'Expected ) after method call');
|
|
795
|
+
|
|
796
|
+
return {
|
|
797
|
+
type: 'MethodCall',
|
|
798
|
+
object,
|
|
799
|
+
method: name.lexeme,
|
|
800
|
+
arguments: args,
|
|
801
|
+
};
|
|
802
|
+
} else {
|
|
803
|
+
throw new Error(
|
|
804
|
+
`Method ${name.lexeme}() does not accept arguments`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
// Consume the closing parenthesis
|
|
809
|
+
this.consume('RIGHT_PAREN', 'Expected ) after method call');
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return {
|
|
813
|
+
type: 'MethodCall',
|
|
814
|
+
object,
|
|
815
|
+
method: name.lexeme,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return {
|
|
820
|
+
type: 'PropertyAccess',
|
|
821
|
+
object,
|
|
822
|
+
name: name.lexeme,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Finishes parsing a function call
|
|
828
|
+
* @param {Object} callee - The function being called
|
|
829
|
+
* @returns {Object} Function call expression
|
|
830
|
+
*/
|
|
831
|
+
finishCall(callee) {
|
|
832
|
+
const args = [];
|
|
833
|
+
|
|
834
|
+
if (!this.check('RIGHT_PAREN')) {
|
|
835
|
+
do {
|
|
836
|
+
if (args.length >= 255) {
|
|
837
|
+
throw new Error('Cannot have more than 255 arguments');
|
|
838
|
+
}
|
|
839
|
+
args.push(this.expression());
|
|
840
|
+
} while (this.match('COMMA'));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const paren = this.consume('RIGHT_PAREN', 'Expected ) after arguments');
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
type: 'Call',
|
|
847
|
+
callee: {
|
|
848
|
+
type: 'Variable',
|
|
849
|
+
name: callee.lexeme,
|
|
850
|
+
},
|
|
851
|
+
arguments: args,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Parses an array literal
|
|
857
|
+
* @returns {Object} Array literal
|
|
858
|
+
*/
|
|
859
|
+
arrayLiteral() {
|
|
860
|
+
const elements = [];
|
|
861
|
+
|
|
862
|
+
if (!this.check('RIGHT_BRACKET')) {
|
|
863
|
+
do {
|
|
864
|
+
elements.push(this.expression());
|
|
865
|
+
} while (this.match('COMMA'));
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
this.consume('RIGHT_BRACKET', 'Expected ] after array elements');
|
|
869
|
+
|
|
870
|
+
return {
|
|
871
|
+
type: 'ArrayLiteral',
|
|
872
|
+
elements,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Parses an object literal
|
|
878
|
+
* @returns {Object} Object literal
|
|
879
|
+
*/
|
|
880
|
+
objectLiteral() {
|
|
881
|
+
const properties = [];
|
|
882
|
+
|
|
883
|
+
if (!this.check('RIGHT_BRACE')) {
|
|
884
|
+
do {
|
|
885
|
+
// Parse property name (identifier or string)
|
|
886
|
+
let name;
|
|
887
|
+
if (this.match('IDENTIFIER')) {
|
|
888
|
+
name = this.previous().lexeme;
|
|
889
|
+
} else if (this.match('STRING')) {
|
|
890
|
+
name = this.previous().literal;
|
|
891
|
+
} else {
|
|
892
|
+
throw new Error('Expected property name');
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
this.consume('COLON', 'Expected : after property name');
|
|
896
|
+
const value = this.expression();
|
|
897
|
+
|
|
898
|
+
properties.push({
|
|
899
|
+
type: 'Property',
|
|
900
|
+
name,
|
|
901
|
+
value,
|
|
902
|
+
});
|
|
903
|
+
} while (this.match('COMMA'));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
this.consume('RIGHT_BRACE', 'Expected } after object properties');
|
|
907
|
+
|
|
908
|
+
return {
|
|
909
|
+
type: 'ObjectLiteral',
|
|
910
|
+
properties,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Checks if the current token matches any of the given types
|
|
916
|
+
* @param {...string} types - Token types to check
|
|
917
|
+
* @returns {boolean} True if it matches
|
|
918
|
+
*/
|
|
919
|
+
match(...types) {
|
|
920
|
+
for (const type of types) {
|
|
921
|
+
if (this.check(type)) {
|
|
922
|
+
this.advance();
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Checks if the current token is of the given type
|
|
931
|
+
* @param {string} type - Token type to check
|
|
932
|
+
* @returns {boolean} True if it is of the type
|
|
933
|
+
*/
|
|
934
|
+
check(type) {
|
|
935
|
+
if (this.isAtEnd()) return false;
|
|
936
|
+
return this.peek().type === type;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Advances to the next token
|
|
941
|
+
* @returns {Object} Previous token
|
|
942
|
+
*/
|
|
943
|
+
advance() {
|
|
944
|
+
if (!this.isAtEnd()) this.current++;
|
|
945
|
+
return this.previous();
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Checks if we reached the end
|
|
950
|
+
* @returns {boolean} True if we reached the end
|
|
951
|
+
*/
|
|
952
|
+
isAtEnd() {
|
|
953
|
+
return this.peek().type === 'EOF';
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Returns the current token
|
|
958
|
+
* @returns {Object} Current token
|
|
959
|
+
*/
|
|
960
|
+
peek() {
|
|
961
|
+
return this.tokens[this.current];
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Returns the previous token
|
|
966
|
+
* @returns {Object} Previous token
|
|
967
|
+
*/
|
|
968
|
+
previous() {
|
|
969
|
+
return this.tokens[this.current - 1];
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Consumes a token of the expected type
|
|
974
|
+
* @param {string} type - Expected token type
|
|
975
|
+
* @param {string} message - Error message if it doesn't match
|
|
976
|
+
* @returns {Object} Consumed token
|
|
977
|
+
*/
|
|
978
|
+
consume(type, message) {
|
|
979
|
+
if (this.check(type)) return this.advance();
|
|
980
|
+
|
|
981
|
+
throw new Error(`${message} at line ${this.peek().line}`);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Synchronizes the parser after an error
|
|
986
|
+
*/
|
|
987
|
+
synchronize() {
|
|
988
|
+
this.advance();
|
|
989
|
+
|
|
990
|
+
while (!this.isAtEnd()) {
|
|
991
|
+
if (this.previous().type === 'EOF') return;
|
|
992
|
+
|
|
993
|
+
switch (this.peek().type) {
|
|
994
|
+
case 'VARIABLE':
|
|
995
|
+
case 'FUNCION':
|
|
996
|
+
case 'MOSTRAR':
|
|
997
|
+
case 'LEER':
|
|
998
|
+
case 'SI':
|
|
999
|
+
case 'MIENTRAS':
|
|
1000
|
+
case 'PARA':
|
|
1001
|
+
case 'RETORNAR':
|
|
1002
|
+
case 'ROMPER':
|
|
1003
|
+
case 'CONTINUAR':
|
|
1004
|
+
case 'INTENTAR':
|
|
1005
|
+
case 'CAPTURAR':
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
this.advance();
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Parses a try-catch statement
|
|
1015
|
+
* @returns {Object} Try-catch statement
|
|
1016
|
+
*/
|
|
1017
|
+
tryStatement() {
|
|
1018
|
+
// Parse the try block
|
|
1019
|
+
this.consume('LEFT_BRACE', 'Expected { after intentar');
|
|
1020
|
+
const tryBlock = this.block();
|
|
1021
|
+
this.consume('RIGHT_BRACE', 'Expected } after intentar block');
|
|
1022
|
+
|
|
1023
|
+
// Look for catch block
|
|
1024
|
+
if (this.match('CAPTURAR')) {
|
|
1025
|
+
// Parse catch parameter (error variable name)
|
|
1026
|
+
this.consume('LEFT_PAREN', 'Expected ( after capturar');
|
|
1027
|
+
const errorVariable = this.consume(
|
|
1028
|
+
'IDENTIFIER',
|
|
1029
|
+
'Expected error variable name'
|
|
1030
|
+
);
|
|
1031
|
+
this.consume('RIGHT_PAREN', 'Expected ) after error variable');
|
|
1032
|
+
|
|
1033
|
+
// Parse catch block
|
|
1034
|
+
this.consume('LEFT_BRACE', 'Expected { after capturar');
|
|
1035
|
+
const catchBlock = this.block();
|
|
1036
|
+
this.consume('RIGHT_BRACE', 'Expected } after capturar block');
|
|
1037
|
+
|
|
1038
|
+
return {
|
|
1039
|
+
type: 'TryCatch',
|
|
1040
|
+
tryBlock,
|
|
1041
|
+
catchBlock,
|
|
1042
|
+
errorVariable: errorVariable.lexeme,
|
|
1043
|
+
};
|
|
1044
|
+
} else {
|
|
1045
|
+
throw new Error('Expected capturar after intentar block');
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
module.exports = Parser;
|