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
|
@@ -0,0 +1,1391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluator for HispanoLang
|
|
3
|
+
* Executes AST statements
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class Evaluator {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.environment = new Environment();
|
|
9
|
+
this.output = [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Evaluates a list of statements
|
|
14
|
+
* @param {Array} statements - List of AST statements
|
|
15
|
+
* @returns {Array} Execution result
|
|
16
|
+
*/
|
|
17
|
+
evaluate(statements) {
|
|
18
|
+
this.output = [];
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
for (const statement of statements) {
|
|
22
|
+
this.execute(statement);
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`Execution error: ${error.message}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return this.output;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Executes a statement
|
|
33
|
+
* @param {Object} statement - Statement to execute
|
|
34
|
+
*/
|
|
35
|
+
execute(statement) {
|
|
36
|
+
switch (statement.type) {
|
|
37
|
+
case 'VariableDeclaration':
|
|
38
|
+
return this.executeVariableDeclaration(statement);
|
|
39
|
+
case 'FunctionDeclaration':
|
|
40
|
+
return this.executeFunctionDeclaration(statement);
|
|
41
|
+
case 'MostrarStatement':
|
|
42
|
+
return this.executeMostrarStatement(statement);
|
|
43
|
+
case 'LeerStatement':
|
|
44
|
+
return this.executeLeerStatement(statement);
|
|
45
|
+
case 'IfStatement':
|
|
46
|
+
return this.executeIfStatement(statement);
|
|
47
|
+
case 'WhileStatement':
|
|
48
|
+
return this.executeWhileStatement(statement);
|
|
49
|
+
case 'ForStatement':
|
|
50
|
+
return this.executeForStatement(statement);
|
|
51
|
+
case 'ReturnStatement':
|
|
52
|
+
return this.executeReturnStatement(statement);
|
|
53
|
+
case 'BreakStatement':
|
|
54
|
+
return this.executeBreakStatement(statement);
|
|
55
|
+
case 'ContinueStatement':
|
|
56
|
+
return this.executeContinueStatement(statement);
|
|
57
|
+
case 'TryCatch':
|
|
58
|
+
return this.executeTryCatch(statement);
|
|
59
|
+
case 'ExpressionStatement':
|
|
60
|
+
return this.executeExpressionStatement(statement);
|
|
61
|
+
case 'Block':
|
|
62
|
+
return this.executeBlock(statement);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Executes a variable declaration
|
|
68
|
+
* @param {Object} statement - Variable declaration
|
|
69
|
+
*/
|
|
70
|
+
executeVariableDeclaration(statement) {
|
|
71
|
+
let value = null;
|
|
72
|
+
if (statement.initializer !== null) {
|
|
73
|
+
value = this.evaluateExpression(statement.initializer);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.environment.define(statement.name, value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Executes a function declaration
|
|
81
|
+
* @param {Object} statement - Function declaration
|
|
82
|
+
*/
|
|
83
|
+
executeFunctionDeclaration(statement) {
|
|
84
|
+
const functionObj = {
|
|
85
|
+
type: 'Function',
|
|
86
|
+
name: statement.name,
|
|
87
|
+
parameters: statement.parameters,
|
|
88
|
+
body: statement.body,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.environment.define(statement.name, functionObj);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Executes a show statement
|
|
96
|
+
* @param {Object} statement - Show statement
|
|
97
|
+
*/
|
|
98
|
+
executeMostrarStatement(statement) {
|
|
99
|
+
const value = this.evaluateExpression(statement.expression);
|
|
100
|
+
const output = this.stringify(value);
|
|
101
|
+
this.output.push(output);
|
|
102
|
+
console.log(output);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Executes a read statement
|
|
107
|
+
* @param {Object} statement - Read statement
|
|
108
|
+
*/
|
|
109
|
+
executeLeerStatement(statement) {
|
|
110
|
+
// Show prompt if provided
|
|
111
|
+
if (statement.prompt) {
|
|
112
|
+
console.log(statement.prompt);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Try different methods for input
|
|
116
|
+
let input = '';
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// Method 1: Try readline-sync
|
|
120
|
+
const readlineSync = require('readline-sync');
|
|
121
|
+
input = readlineSync.question('');
|
|
122
|
+
} catch (error1) {
|
|
123
|
+
try {
|
|
124
|
+
// Method 2: Try fs.readFileSync with stdin
|
|
125
|
+
const fs = require('fs');
|
|
126
|
+
input = fs.readFileSync(0, 'utf8').trim();
|
|
127
|
+
} catch (error2) {
|
|
128
|
+
try {
|
|
129
|
+
// Method 3: Try process.stdin
|
|
130
|
+
const readline = require('readline');
|
|
131
|
+
const rl = readline.createInterface({
|
|
132
|
+
input: process.stdin,
|
|
133
|
+
output: process.stdout,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// This is a simplified approach - in a real implementation
|
|
137
|
+
// you'd need to handle this asynchronously
|
|
138
|
+
input = '';
|
|
139
|
+
} catch (error3) {
|
|
140
|
+
// Fallback: use empty string
|
|
141
|
+
input = '';
|
|
142
|
+
console.log('(Entrada no disponible en este entorno)');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Try to parse as number first, then as string
|
|
148
|
+
let value = input;
|
|
149
|
+
if (!isNaN(input) && input.trim() !== '') {
|
|
150
|
+
value = parseFloat(input);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Store the value in the environment
|
|
154
|
+
this.environment.define(statement.variable, value);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Executes an if statement
|
|
159
|
+
* @param {Object} statement - If statement
|
|
160
|
+
*/
|
|
161
|
+
executeIfStatement(statement) {
|
|
162
|
+
if (this.isTruthy(this.evaluateExpression(statement.condition))) {
|
|
163
|
+
this.executeBlock(statement.thenBranch);
|
|
164
|
+
} else if (statement.elseBranch !== null) {
|
|
165
|
+
this.executeBlock(statement.elseBranch);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Executes a while statement
|
|
171
|
+
* @param {Object} statement - While statement
|
|
172
|
+
*/
|
|
173
|
+
executeWhileStatement(statement) {
|
|
174
|
+
while (this.isTruthy(this.evaluateExpression(statement.condition))) {
|
|
175
|
+
try {
|
|
176
|
+
this.executeBlock(statement.body);
|
|
177
|
+
} catch (breakException) {
|
|
178
|
+
if (breakException instanceof BreakException) {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
if (breakException instanceof ContinueException) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
throw breakException;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Executes a for statement
|
|
191
|
+
* @param {Object} statement - For statement
|
|
192
|
+
*/
|
|
193
|
+
executeForStatement(statement) {
|
|
194
|
+
// Execute initializer
|
|
195
|
+
if (statement.initializer !== null) {
|
|
196
|
+
this.execute(statement.initializer);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Execute loop
|
|
200
|
+
// If no condition is provided, don't execute the loop (like JavaScript)
|
|
201
|
+
if (statement.condition !== null) {
|
|
202
|
+
while (this.isTruthy(this.evaluateExpression(statement.condition))) {
|
|
203
|
+
try {
|
|
204
|
+
this.executeBlock(statement.body);
|
|
205
|
+
} catch (breakException) {
|
|
206
|
+
if (breakException instanceof BreakException) {
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
if (breakException instanceof ContinueException) {
|
|
210
|
+
// Execute increment before continuing
|
|
211
|
+
if (statement.increment !== null) {
|
|
212
|
+
this.evaluateExpression(statement.increment);
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
throw breakException;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Execute increment
|
|
220
|
+
if (statement.increment !== null) {
|
|
221
|
+
this.evaluateExpression(statement.increment);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Executes a return statement
|
|
229
|
+
* @param {Object} statement - Return statement
|
|
230
|
+
*/
|
|
231
|
+
executeReturnStatement(statement) {
|
|
232
|
+
let value = null;
|
|
233
|
+
if (statement.value !== null) {
|
|
234
|
+
value = this.evaluateExpression(statement.value);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
throw new ReturnException(value);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Executes a break statement
|
|
242
|
+
* @param {Object} statement - Break statement
|
|
243
|
+
*/
|
|
244
|
+
executeBreakStatement(statement) {
|
|
245
|
+
throw new BreakException();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Executes a continue statement
|
|
250
|
+
* @param {Object} statement - Continue statement
|
|
251
|
+
*/
|
|
252
|
+
executeContinueStatement(statement) {
|
|
253
|
+
throw new ContinueException();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Executes a block of statements
|
|
258
|
+
* @param {Array} statements - List of statements in the block
|
|
259
|
+
*/
|
|
260
|
+
executeBlock(statements) {
|
|
261
|
+
const previous = this.environment;
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
this.environment = new Environment(previous);
|
|
265
|
+
|
|
266
|
+
for (const statement of statements) {
|
|
267
|
+
this.execute(statement);
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
this.environment = previous;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Executes an expression statement
|
|
276
|
+
* @param {Object} statement - Expression statement
|
|
277
|
+
*/
|
|
278
|
+
executeExpressionStatement(statement) {
|
|
279
|
+
return this.evaluateExpression(statement.expression);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Evaluates an expression
|
|
284
|
+
* @param {Object} expression - Expression to evaluate
|
|
285
|
+
* @returns {any} Expression result
|
|
286
|
+
*/
|
|
287
|
+
evaluateExpression(expression) {
|
|
288
|
+
switch (expression.type) {
|
|
289
|
+
case 'Literal':
|
|
290
|
+
return expression.value;
|
|
291
|
+
|
|
292
|
+
case 'Variable':
|
|
293
|
+
return this.environment.get(expression.name);
|
|
294
|
+
|
|
295
|
+
case 'AnonymousFunction':
|
|
296
|
+
return {
|
|
297
|
+
type: 'Function',
|
|
298
|
+
name: null,
|
|
299
|
+
parameters: expression.parameters,
|
|
300
|
+
body: expression.body,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
case 'Assign':
|
|
304
|
+
const value = this.evaluateExpression(expression.value);
|
|
305
|
+
this.environment.assign(expression.name, value);
|
|
306
|
+
return value;
|
|
307
|
+
|
|
308
|
+
case 'ArrayLiteral':
|
|
309
|
+
return this.evaluateArrayLiteral(expression);
|
|
310
|
+
|
|
311
|
+
case 'ArrayAccess':
|
|
312
|
+
return this.evaluateArrayAccess(expression);
|
|
313
|
+
|
|
314
|
+
case 'ArrayAssign':
|
|
315
|
+
return this.evaluateArrayAssign(expression);
|
|
316
|
+
|
|
317
|
+
case 'ObjectLiteral':
|
|
318
|
+
return this.evaluateObjectLiteral(expression);
|
|
319
|
+
|
|
320
|
+
case 'PropertyAccess':
|
|
321
|
+
return this.evaluatePropertyAccess(expression);
|
|
322
|
+
|
|
323
|
+
case 'PropertyAssign':
|
|
324
|
+
return this.evaluatePropertyAssign(expression);
|
|
325
|
+
|
|
326
|
+
case 'CompoundAssign':
|
|
327
|
+
return this.evaluateCompoundAssign(expression);
|
|
328
|
+
|
|
329
|
+
case 'CompoundArrayAssign':
|
|
330
|
+
return this.evaluateCompoundArrayAssign(expression);
|
|
331
|
+
|
|
332
|
+
case 'CompoundPropertyAssign':
|
|
333
|
+
return this.evaluateCompoundPropertyAssign(expression);
|
|
334
|
+
|
|
335
|
+
case 'Logical':
|
|
336
|
+
return this.evaluateLogicalExpression(expression);
|
|
337
|
+
|
|
338
|
+
case 'Postfix':
|
|
339
|
+
return this.evaluatePostfixExpression(expression);
|
|
340
|
+
|
|
341
|
+
case 'Call':
|
|
342
|
+
return this.evaluateCallExpression(expression);
|
|
343
|
+
|
|
344
|
+
case 'MethodCall':
|
|
345
|
+
return this.evaluateMethodCall(expression);
|
|
346
|
+
|
|
347
|
+
case 'Unary':
|
|
348
|
+
const right = this.evaluateExpression(expression.right);
|
|
349
|
+
return this.evaluateUnaryExpression(expression.operator, right);
|
|
350
|
+
|
|
351
|
+
case 'Binary':
|
|
352
|
+
const left = this.evaluateExpression(expression.left);
|
|
353
|
+
const rightOperand = this.evaluateExpression(expression.right);
|
|
354
|
+
return this.evaluateBinaryExpression(
|
|
355
|
+
left,
|
|
356
|
+
expression.operator,
|
|
357
|
+
rightOperand
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
default:
|
|
361
|
+
throw new Error(`Unrecognized expression type: ${expression.type}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Evaluates a unary expression
|
|
367
|
+
* @param {string} operator - Unary operator
|
|
368
|
+
* @param {any} right - Right operand
|
|
369
|
+
* @returns {any} Expression result
|
|
370
|
+
*/
|
|
371
|
+
evaluateUnaryExpression(operator, right) {
|
|
372
|
+
switch (operator) {
|
|
373
|
+
case 'MINUS':
|
|
374
|
+
this.checkNumberOperand(operator, right);
|
|
375
|
+
return -right;
|
|
376
|
+
|
|
377
|
+
case 'BANG':
|
|
378
|
+
return !this.isTruthy(right);
|
|
379
|
+
|
|
380
|
+
default:
|
|
381
|
+
throw new Error(`Unrecognized unary operator: ${operator}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Evaluates a binary expression
|
|
387
|
+
* @param {any} left - Left operand
|
|
388
|
+
* @param {string} operator - Binary operator
|
|
389
|
+
* @param {any} right - Right operand
|
|
390
|
+
* @returns {any} Expression result
|
|
391
|
+
*/
|
|
392
|
+
evaluateBinaryExpression(left, operator, right) {
|
|
393
|
+
switch (operator) {
|
|
394
|
+
case 'MINUS':
|
|
395
|
+
this.checkNumberOperands(operator, left, right);
|
|
396
|
+
return left - right;
|
|
397
|
+
|
|
398
|
+
case 'PLUS':
|
|
399
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
400
|
+
return left + right;
|
|
401
|
+
}
|
|
402
|
+
// Convert numbers to strings for concatenation
|
|
403
|
+
const leftStr = this.stringify(left);
|
|
404
|
+
const rightStr = this.stringify(right);
|
|
405
|
+
return leftStr + rightStr;
|
|
406
|
+
|
|
407
|
+
case 'SLASH':
|
|
408
|
+
this.checkNumberOperands(operator, left, right);
|
|
409
|
+
if (right === 0) {
|
|
410
|
+
throw new Error('Division by zero');
|
|
411
|
+
}
|
|
412
|
+
return left / right;
|
|
413
|
+
|
|
414
|
+
case 'STAR':
|
|
415
|
+
this.checkNumberOperands(operator, left, right);
|
|
416
|
+
return left * right;
|
|
417
|
+
|
|
418
|
+
case 'PERCENT':
|
|
419
|
+
this.checkNumberOperands(operator, left, right);
|
|
420
|
+
if (right === 0) {
|
|
421
|
+
throw new Error('Modulo by zero');
|
|
422
|
+
}
|
|
423
|
+
return left % right;
|
|
424
|
+
|
|
425
|
+
case 'GREATER':
|
|
426
|
+
if (typeof left === 'string' && typeof right === 'string') {
|
|
427
|
+
return left > right;
|
|
428
|
+
}
|
|
429
|
+
this.checkNumberOperands(operator, left, right);
|
|
430
|
+
return left > right;
|
|
431
|
+
|
|
432
|
+
case 'GREATER_EQUAL':
|
|
433
|
+
if (typeof left === 'string' && typeof right === 'string') {
|
|
434
|
+
return left >= right;
|
|
435
|
+
}
|
|
436
|
+
this.checkNumberOperands(operator, left, right);
|
|
437
|
+
return left >= right;
|
|
438
|
+
|
|
439
|
+
case 'LESS':
|
|
440
|
+
if (typeof left === 'string' && typeof right === 'string') {
|
|
441
|
+
return left < right;
|
|
442
|
+
}
|
|
443
|
+
this.checkNumberOperands(operator, left, right);
|
|
444
|
+
return left < right;
|
|
445
|
+
|
|
446
|
+
case 'LESS_EQUAL':
|
|
447
|
+
if (typeof left === 'string' && typeof right === 'string') {
|
|
448
|
+
return left <= right;
|
|
449
|
+
}
|
|
450
|
+
this.checkNumberOperands(operator, left, right);
|
|
451
|
+
return left <= right;
|
|
452
|
+
|
|
453
|
+
case 'EQUAL_EQUAL':
|
|
454
|
+
return this.isEqual(left, right);
|
|
455
|
+
|
|
456
|
+
case 'BANG_EQUAL':
|
|
457
|
+
return !this.isEqual(left, right);
|
|
458
|
+
|
|
459
|
+
default:
|
|
460
|
+
throw new Error(`Unrecognized binary operator: ${operator}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Checks if a value is truthy
|
|
466
|
+
* @param {any} value - Value to check
|
|
467
|
+
* @returns {boolean} True if it is truthy
|
|
468
|
+
*/
|
|
469
|
+
isTruthy(value) {
|
|
470
|
+
if (value === null) return false;
|
|
471
|
+
if (typeof value === 'boolean') return value;
|
|
472
|
+
if (typeof value === 'number') return value !== 0;
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Checks if two values are equal
|
|
478
|
+
* @param {any} left - Left value
|
|
479
|
+
* @param {any} right - Right value
|
|
480
|
+
* @returns {boolean} True if they are equal
|
|
481
|
+
*/
|
|
482
|
+
isEqual(left, right) {
|
|
483
|
+
return left === right;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Checks that an operand is a number
|
|
488
|
+
* @param {string} operator - Operator
|
|
489
|
+
* @param {any} operand - Operand
|
|
490
|
+
*/
|
|
491
|
+
checkNumberOperand(operator, operand) {
|
|
492
|
+
if (typeof operand === 'number') return;
|
|
493
|
+
throw new Error(`Operator ${operator} requires a number`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Checks that two operands are numbers
|
|
498
|
+
* @param {string} operator - Operator
|
|
499
|
+
* @param {any} left - Left operand
|
|
500
|
+
* @param {any} right - Right operand
|
|
501
|
+
*/
|
|
502
|
+
checkNumberOperands(operator, left, right) {
|
|
503
|
+
if (typeof left === 'number' && typeof right === 'number') return;
|
|
504
|
+
throw new Error(`Operator ${operator} requires two numbers`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Evaluates a function call
|
|
509
|
+
* @param {Object} expression - Call expression
|
|
510
|
+
* @returns {any} Function result
|
|
511
|
+
*/
|
|
512
|
+
evaluateCallExpression(expression) {
|
|
513
|
+
// Check if it's a built-in mathematical function BEFORE evaluating the callee
|
|
514
|
+
if (
|
|
515
|
+
expression.callee.type === 'Variable' &&
|
|
516
|
+
this.isMathFunction(expression.callee.name)
|
|
517
|
+
) {
|
|
518
|
+
const args = [];
|
|
519
|
+
for (const argument of expression.arguments) {
|
|
520
|
+
args.push(this.evaluateExpression(argument));
|
|
521
|
+
}
|
|
522
|
+
return this.evaluateMathFunction(expression.callee.name, args);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const callee = this.evaluateExpression(expression.callee);
|
|
526
|
+
const args = [];
|
|
527
|
+
|
|
528
|
+
for (const argument of expression.arguments) {
|
|
529
|
+
args.push(this.evaluateExpression(argument));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (callee.type !== 'Function') {
|
|
533
|
+
throw new Error('Can only call functions');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (args.length !== callee.parameters.length) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
`Expected ${callee.parameters.length} arguments but got ${args.length}`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const environment = new Environment(this.environment);
|
|
543
|
+
|
|
544
|
+
for (let i = 0; i < args.length; i++) {
|
|
545
|
+
environment.define(callee.parameters[i], args[i]);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const previous = this.environment;
|
|
549
|
+
try {
|
|
550
|
+
this.environment = environment;
|
|
551
|
+
this.executeBlock(callee.body);
|
|
552
|
+
return null;
|
|
553
|
+
} catch (returnValue) {
|
|
554
|
+
if (returnValue instanceof ReturnException) {
|
|
555
|
+
return returnValue.value;
|
|
556
|
+
}
|
|
557
|
+
throw returnValue;
|
|
558
|
+
} finally {
|
|
559
|
+
this.environment = previous;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Evaluates a method call (array or string)
|
|
565
|
+
* @param {Object} expression - Method call expression
|
|
566
|
+
* @returns {any} Method result
|
|
567
|
+
*/
|
|
568
|
+
evaluateMethodCall(expression) {
|
|
569
|
+
const object = this.evaluateExpression(expression.object);
|
|
570
|
+
|
|
571
|
+
// Determine if it's an array or string method based on the object type
|
|
572
|
+
if (Array.isArray(object)) {
|
|
573
|
+
return this.evaluateArrayMethod(
|
|
574
|
+
object,
|
|
575
|
+
expression.method,
|
|
576
|
+
expression.arguments
|
|
577
|
+
);
|
|
578
|
+
} else if (typeof object === 'string') {
|
|
579
|
+
// Check if the method is valid for strings
|
|
580
|
+
if (
|
|
581
|
+
expression.method === 'agregar' ||
|
|
582
|
+
expression.method === 'remover' ||
|
|
583
|
+
expression.method === 'contiene' ||
|
|
584
|
+
expression.method === 'recorrer'
|
|
585
|
+
) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
`Method ${expression.method}() can only be called on arrays`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
return this.evaluateStringMethod(object, expression.method);
|
|
591
|
+
} else {
|
|
592
|
+
throw new Error(
|
|
593
|
+
`Can only call methods on arrays or strings, got ${typeof object}`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Evaluates an array method
|
|
600
|
+
* @param {Array} array - The array object
|
|
601
|
+
* @param {string} method - The method name
|
|
602
|
+
* @param {Array} args - Method arguments (optional)
|
|
603
|
+
* @returns {any} Method result
|
|
604
|
+
*/
|
|
605
|
+
evaluateArrayMethod(array, method, args = []) {
|
|
606
|
+
switch (method) {
|
|
607
|
+
case 'longitud':
|
|
608
|
+
return array.length;
|
|
609
|
+
|
|
610
|
+
case 'primero':
|
|
611
|
+
if (array.length === 0) {
|
|
612
|
+
throw new Error('Cannot get first element of empty array');
|
|
613
|
+
}
|
|
614
|
+
return array[0];
|
|
615
|
+
|
|
616
|
+
case 'ultimo':
|
|
617
|
+
if (array.length === 0) {
|
|
618
|
+
throw new Error('Cannot get last element of empty array');
|
|
619
|
+
}
|
|
620
|
+
return array[array.length - 1];
|
|
621
|
+
|
|
622
|
+
case 'agregar':
|
|
623
|
+
// Evaluate all arguments and add them to the array
|
|
624
|
+
for (const arg of args) {
|
|
625
|
+
const value = this.evaluateExpression(arg);
|
|
626
|
+
array.push(value);
|
|
627
|
+
}
|
|
628
|
+
return array.length; // Return the new length
|
|
629
|
+
|
|
630
|
+
case 'remover':
|
|
631
|
+
// Remove and return the last element
|
|
632
|
+
if (array.length === 0) {
|
|
633
|
+
throw new Error('Cannot remove element from empty array');
|
|
634
|
+
}
|
|
635
|
+
return array.pop(); // Return the removed element
|
|
636
|
+
|
|
637
|
+
case 'contiene':
|
|
638
|
+
// Check if array contains the specified element
|
|
639
|
+
if (args.length !== 1) {
|
|
640
|
+
throw new Error('Method contiene() requires exactly one argument');
|
|
641
|
+
}
|
|
642
|
+
const searchElement = this.evaluateExpression(args[0]);
|
|
643
|
+
return array.includes(searchElement);
|
|
644
|
+
|
|
645
|
+
case 'recorrer':
|
|
646
|
+
// Iterate through array and call function for each element
|
|
647
|
+
if (args.length !== 1) {
|
|
648
|
+
throw new Error('Method recorrer() requires exactly one argument');
|
|
649
|
+
}
|
|
650
|
+
const callback = this.evaluateExpression(args[0]);
|
|
651
|
+
if (callback.type !== 'Function') {
|
|
652
|
+
throw new Error('Method recorrer() requires a function as argument');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Call the function for each element
|
|
656
|
+
for (let i = 0; i < array.length; i++) {
|
|
657
|
+
// Create a new environment for the callback function
|
|
658
|
+
const callbackEnv = new Environment(this.environment);
|
|
659
|
+
|
|
660
|
+
// Handle function parameters
|
|
661
|
+
if (callback.parameters.length === 0) {
|
|
662
|
+
// No parameters - use automatic variables
|
|
663
|
+
callbackEnv.define('elemento', array[i]);
|
|
664
|
+
callbackEnv.define('indice', i);
|
|
665
|
+
} else if (callback.parameters.length === 1) {
|
|
666
|
+
// One parameter - element only
|
|
667
|
+
callbackEnv.define(callback.parameters[0], array[i]);
|
|
668
|
+
callbackEnv.define('indice', i);
|
|
669
|
+
} else if (callback.parameters.length === 2) {
|
|
670
|
+
// Two parameters - element and index
|
|
671
|
+
callbackEnv.define(callback.parameters[0], array[i]);
|
|
672
|
+
callbackEnv.define(callback.parameters[1], i);
|
|
673
|
+
} else {
|
|
674
|
+
throw new Error(
|
|
675
|
+
'Function in recorrer() can have at most 2 parameters'
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Execute the callback function
|
|
680
|
+
const previousEnv = this.environment;
|
|
681
|
+
this.environment = callbackEnv;
|
|
682
|
+
try {
|
|
683
|
+
this.executeBlock(callback.body);
|
|
684
|
+
} finally {
|
|
685
|
+
this.environment = previousEnv;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return null; // forEach doesn't return anything
|
|
689
|
+
|
|
690
|
+
default:
|
|
691
|
+
throw new Error(`Unknown array method: ${method}`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Evaluates a string method
|
|
697
|
+
* @param {string} string - The string object
|
|
698
|
+
* @param {string} method - The method name
|
|
699
|
+
* @returns {any} Method result
|
|
700
|
+
*/
|
|
701
|
+
evaluateStringMethod(string, method) {
|
|
702
|
+
switch (method) {
|
|
703
|
+
case 'longitud':
|
|
704
|
+
return string.length;
|
|
705
|
+
|
|
706
|
+
case 'mayusculas':
|
|
707
|
+
return string.toUpperCase();
|
|
708
|
+
|
|
709
|
+
case 'minusculas':
|
|
710
|
+
return string.toLowerCase();
|
|
711
|
+
|
|
712
|
+
default:
|
|
713
|
+
throw new Error(`Unknown string method: ${method}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Checks if a function name is a built-in mathematical function
|
|
719
|
+
* @param {string} name - Function name
|
|
720
|
+
* @returns {boolean} True if it's a math function
|
|
721
|
+
*/
|
|
722
|
+
isMathFunction(name) {
|
|
723
|
+
const mathFunctions = [
|
|
724
|
+
'raiz',
|
|
725
|
+
'potencia',
|
|
726
|
+
'seno',
|
|
727
|
+
'coseno',
|
|
728
|
+
'tangente',
|
|
729
|
+
'logaritmo',
|
|
730
|
+
'valorAbsoluto',
|
|
731
|
+
'redondear',
|
|
732
|
+
'techo',
|
|
733
|
+
'piso',
|
|
734
|
+
'aleatorio',
|
|
735
|
+
'maximo',
|
|
736
|
+
'minimo',
|
|
737
|
+
'suma',
|
|
738
|
+
'promedio',
|
|
739
|
+
];
|
|
740
|
+
return mathFunctions.includes(name);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Evaluates a mathematical function
|
|
745
|
+
* @param {string} name - Function name
|
|
746
|
+
* @param {Array} args - Function arguments
|
|
747
|
+
* @returns {number} Function result
|
|
748
|
+
*/
|
|
749
|
+
evaluateMathFunction(name, args) {
|
|
750
|
+
switch (name) {
|
|
751
|
+
case 'raiz':
|
|
752
|
+
if (args.length !== 1) {
|
|
753
|
+
throw new Error('raiz() requires exactly 1 argument');
|
|
754
|
+
}
|
|
755
|
+
if (typeof args[0] !== 'number') {
|
|
756
|
+
throw new Error('raiz() requires a number argument');
|
|
757
|
+
}
|
|
758
|
+
if (args[0] < 0) {
|
|
759
|
+
throw new Error('Cannot take square root of negative number');
|
|
760
|
+
}
|
|
761
|
+
return Math.sqrt(args[0]);
|
|
762
|
+
|
|
763
|
+
case 'potencia':
|
|
764
|
+
if (args.length !== 2) {
|
|
765
|
+
throw new Error('potencia() requires exactly 2 arguments');
|
|
766
|
+
}
|
|
767
|
+
if (typeof args[0] !== 'number' || typeof args[1] !== 'number') {
|
|
768
|
+
throw new Error('potencia() requires number arguments');
|
|
769
|
+
}
|
|
770
|
+
return Math.pow(args[0], args[1]);
|
|
771
|
+
|
|
772
|
+
case 'seno':
|
|
773
|
+
if (args.length !== 1) {
|
|
774
|
+
throw new Error('seno() requires exactly 1 argument');
|
|
775
|
+
}
|
|
776
|
+
if (typeof args[0] !== 'number') {
|
|
777
|
+
throw new Error('seno() requires a number argument');
|
|
778
|
+
}
|
|
779
|
+
return Math.sin(args[0]);
|
|
780
|
+
|
|
781
|
+
case 'coseno':
|
|
782
|
+
if (args.length !== 1) {
|
|
783
|
+
throw new Error('coseno() requires exactly 1 argument');
|
|
784
|
+
}
|
|
785
|
+
if (typeof args[0] !== 'number') {
|
|
786
|
+
throw new Error('coseno() requires a number argument');
|
|
787
|
+
}
|
|
788
|
+
return Math.cos(args[0]);
|
|
789
|
+
|
|
790
|
+
case 'tangente':
|
|
791
|
+
if (args.length !== 1) {
|
|
792
|
+
throw new Error('tangente() requires exactly 1 argument');
|
|
793
|
+
}
|
|
794
|
+
if (typeof args[0] !== 'number') {
|
|
795
|
+
throw new Error('tangente() requires a number argument');
|
|
796
|
+
}
|
|
797
|
+
return Math.tan(args[0]);
|
|
798
|
+
|
|
799
|
+
case 'logaritmo':
|
|
800
|
+
if (args.length !== 1) {
|
|
801
|
+
throw new Error('logaritmo() requires exactly 1 argument');
|
|
802
|
+
}
|
|
803
|
+
if (typeof args[0] !== 'number') {
|
|
804
|
+
throw new Error('logaritmo() requires a number argument');
|
|
805
|
+
}
|
|
806
|
+
if (args[0] <= 0) {
|
|
807
|
+
throw new Error('Cannot take logarithm of non-positive number');
|
|
808
|
+
}
|
|
809
|
+
return Math.log(args[0]);
|
|
810
|
+
|
|
811
|
+
case 'valorAbsoluto':
|
|
812
|
+
if (args.length !== 1) {
|
|
813
|
+
throw new Error('valorAbsoluto() requires exactly 1 argument');
|
|
814
|
+
}
|
|
815
|
+
if (typeof args[0] !== 'number') {
|
|
816
|
+
throw new Error('valorAbsoluto() requires a number argument');
|
|
817
|
+
}
|
|
818
|
+
return Math.abs(args[0]);
|
|
819
|
+
|
|
820
|
+
case 'redondear':
|
|
821
|
+
if (args.length !== 1) {
|
|
822
|
+
throw new Error('redondear() requires exactly 1 argument');
|
|
823
|
+
}
|
|
824
|
+
if (typeof args[0] !== 'number') {
|
|
825
|
+
throw new Error('redondear() requires a number argument');
|
|
826
|
+
}
|
|
827
|
+
return Math.round(args[0]);
|
|
828
|
+
|
|
829
|
+
case 'techo':
|
|
830
|
+
if (args.length !== 1) {
|
|
831
|
+
throw new Error('techo() requires exactly 1 argument');
|
|
832
|
+
}
|
|
833
|
+
if (typeof args[0] !== 'number') {
|
|
834
|
+
throw new Error('techo() requires a number argument');
|
|
835
|
+
}
|
|
836
|
+
return Math.ceil(args[0]);
|
|
837
|
+
|
|
838
|
+
case 'piso':
|
|
839
|
+
if (args.length !== 1) {
|
|
840
|
+
throw new Error('piso() requires exactly 1 argument');
|
|
841
|
+
}
|
|
842
|
+
if (typeof args[0] !== 'number') {
|
|
843
|
+
throw new Error('piso() requires a number argument');
|
|
844
|
+
}
|
|
845
|
+
return Math.floor(args[0]);
|
|
846
|
+
|
|
847
|
+
case 'aleatorio':
|
|
848
|
+
if (args.length === 0) {
|
|
849
|
+
return Math.random();
|
|
850
|
+
} else if (args.length === 1) {
|
|
851
|
+
if (typeof args[0] !== 'number') {
|
|
852
|
+
throw new Error('aleatorio() requires a number argument');
|
|
853
|
+
}
|
|
854
|
+
return Math.random() * args[0];
|
|
855
|
+
} else if (args.length === 2) {
|
|
856
|
+
if (typeof args[0] !== 'number' || typeof args[1] !== 'number') {
|
|
857
|
+
throw new Error('aleatorio() requires number arguments');
|
|
858
|
+
}
|
|
859
|
+
return Math.random() * (args[1] - args[0]) + args[0];
|
|
860
|
+
} else {
|
|
861
|
+
throw new Error('aleatorio() accepts 0, 1, or 2 arguments');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
case 'maximo':
|
|
865
|
+
if (args.length < 1) {
|
|
866
|
+
throw new Error('maximo() requires at least 1 argument');
|
|
867
|
+
}
|
|
868
|
+
for (const arg of args) {
|
|
869
|
+
if (typeof arg !== 'number') {
|
|
870
|
+
throw new Error('maximo() requires number arguments');
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return Math.max(...args);
|
|
874
|
+
|
|
875
|
+
case 'minimo':
|
|
876
|
+
if (args.length < 1) {
|
|
877
|
+
throw new Error('minimo() requires at least 1 argument');
|
|
878
|
+
}
|
|
879
|
+
for (const arg of args) {
|
|
880
|
+
if (typeof arg !== 'number') {
|
|
881
|
+
throw new Error('minimo() requires number arguments');
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return Math.min(...args);
|
|
885
|
+
|
|
886
|
+
case 'suma':
|
|
887
|
+
if (args.length < 1) {
|
|
888
|
+
throw new Error('suma() requires at least 1 argument');
|
|
889
|
+
}
|
|
890
|
+
for (const arg of args) {
|
|
891
|
+
if (typeof arg !== 'number') {
|
|
892
|
+
throw new Error('suma() requires number arguments');
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return args.reduce((sum, arg) => sum + arg, 0);
|
|
896
|
+
|
|
897
|
+
case 'promedio':
|
|
898
|
+
if (args.length < 1) {
|
|
899
|
+
throw new Error('promedio() requires at least 1 argument');
|
|
900
|
+
}
|
|
901
|
+
for (const arg of args) {
|
|
902
|
+
if (typeof arg !== 'number') {
|
|
903
|
+
throw new Error('promedio() requires number arguments');
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return args.reduce((sum, arg) => sum + arg, 0) / args.length;
|
|
907
|
+
|
|
908
|
+
default:
|
|
909
|
+
throw new Error(`Unknown mathematical function: ${name}`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Evaluates an array literal
|
|
915
|
+
* @param {Object} expression - Array literal expression
|
|
916
|
+
* @returns {Array} Array value
|
|
917
|
+
*/
|
|
918
|
+
evaluateArrayLiteral(expression) {
|
|
919
|
+
const elements = [];
|
|
920
|
+
for (const element of expression.elements) {
|
|
921
|
+
elements.push(this.evaluateExpression(element));
|
|
922
|
+
}
|
|
923
|
+
return elements;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Evaluates array access
|
|
928
|
+
* @param {Object} expression - Array access expression
|
|
929
|
+
* @returns {any} Array element value
|
|
930
|
+
*/
|
|
931
|
+
evaluateArrayAccess(expression) {
|
|
932
|
+
const array = this.evaluateExpression(expression.array);
|
|
933
|
+
const index = this.evaluateExpression(expression.index);
|
|
934
|
+
|
|
935
|
+
if (!Array.isArray(array)) {
|
|
936
|
+
throw new Error('Can only access elements of arrays');
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (typeof index !== 'number') {
|
|
940
|
+
throw new Error('Array index must be a number');
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (index < 0 || index >= array.length) {
|
|
944
|
+
throw new Error('Array index out of bounds');
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return array[index];
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Evaluates array assignment
|
|
952
|
+
* @param {Object} expression - Array assignment expression
|
|
953
|
+
* @returns {any} Assigned value
|
|
954
|
+
*/
|
|
955
|
+
evaluateArrayAssign(expression) {
|
|
956
|
+
const array = this.evaluateExpression(expression.array);
|
|
957
|
+
const index = this.evaluateExpression(expression.index);
|
|
958
|
+
const value = this.evaluateExpression(expression.value);
|
|
959
|
+
|
|
960
|
+
if (!Array.isArray(array)) {
|
|
961
|
+
throw new Error('Can only assign to elements of arrays');
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (typeof index !== 'number') {
|
|
965
|
+
throw new Error('Array index must be a number');
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (index < 0 || index >= array.length) {
|
|
969
|
+
throw new Error('Array index out of bounds');
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
array[index] = value;
|
|
973
|
+
return value;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Evaluates object literal
|
|
978
|
+
* @param {Object} expression - Object literal expression
|
|
979
|
+
* @returns {Object} Object value
|
|
980
|
+
*/
|
|
981
|
+
evaluateObjectLiteral(expression) {
|
|
982
|
+
const object = {};
|
|
983
|
+
|
|
984
|
+
for (const property of expression.properties) {
|
|
985
|
+
const value = this.evaluateExpression(property.value);
|
|
986
|
+
object[property.name] = value;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return object;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Evaluates property access
|
|
994
|
+
* @param {Object} expression - Property access expression
|
|
995
|
+
* @returns {any} Property value
|
|
996
|
+
*/
|
|
997
|
+
evaluatePropertyAccess(expression) {
|
|
998
|
+
const object = this.evaluateExpression(expression.object);
|
|
999
|
+
|
|
1000
|
+
if (typeof object !== 'object' || object === null) {
|
|
1001
|
+
throw new Error('Can only access properties of objects');
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return object[expression.name];
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Evaluates property assignment
|
|
1009
|
+
* @param {Object} expression - Property assignment expression
|
|
1010
|
+
* @returns {any} Assigned value
|
|
1011
|
+
*/
|
|
1012
|
+
evaluatePropertyAssign(expression) {
|
|
1013
|
+
const object = this.evaluateExpression(expression.object);
|
|
1014
|
+
const value = this.evaluateExpression(expression.value);
|
|
1015
|
+
|
|
1016
|
+
if (typeof object !== 'object' || object === null) {
|
|
1017
|
+
throw new Error('Can only assign to properties of objects');
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
object[expression.name] = value;
|
|
1021
|
+
return value;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Evaluates logical expression
|
|
1026
|
+
* @param {Object} expression - Logical expression
|
|
1027
|
+
* @returns {any} Logical result
|
|
1028
|
+
*/
|
|
1029
|
+
evaluateLogicalExpression(expression) {
|
|
1030
|
+
const left = this.evaluateExpression(expression.left);
|
|
1031
|
+
|
|
1032
|
+
if (expression.operator === 'OR') {
|
|
1033
|
+
// Short-circuit evaluation for OR
|
|
1034
|
+
if (this.isTruthy(left)) {
|
|
1035
|
+
return left;
|
|
1036
|
+
}
|
|
1037
|
+
return this.evaluateExpression(expression.right);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (expression.operator === 'AND') {
|
|
1041
|
+
// Short-circuit evaluation for AND
|
|
1042
|
+
if (!this.isTruthy(left)) {
|
|
1043
|
+
return left;
|
|
1044
|
+
}
|
|
1045
|
+
return this.evaluateExpression(expression.right);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
throw new Error(`Unknown logical operator: ${expression.operator}`);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Evaluates compound assignment
|
|
1053
|
+
* @param {Object} expression - Compound assignment expression
|
|
1054
|
+
* @returns {any} Assignment result
|
|
1055
|
+
*/
|
|
1056
|
+
evaluateCompoundAssign(expression) {
|
|
1057
|
+
const currentValue = this.environment.get(expression.name);
|
|
1058
|
+
const rightValue = this.evaluateExpression(expression.value);
|
|
1059
|
+
const newValue = this.performCompoundOperation(
|
|
1060
|
+
currentValue,
|
|
1061
|
+
expression.operator,
|
|
1062
|
+
rightValue
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
this.environment.assign(expression.name, newValue);
|
|
1066
|
+
return newValue;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Evaluates compound array assignment
|
|
1071
|
+
* @param {Object} expression - Compound array assignment expression
|
|
1072
|
+
* @returns {any} Assignment result
|
|
1073
|
+
*/
|
|
1074
|
+
evaluateCompoundArrayAssign(expression) {
|
|
1075
|
+
const array = this.evaluateExpression(expression.array);
|
|
1076
|
+
const index = this.evaluateExpression(expression.index);
|
|
1077
|
+
const rightValue = this.evaluateExpression(expression.value);
|
|
1078
|
+
|
|
1079
|
+
if (!Array.isArray(array)) {
|
|
1080
|
+
throw new Error('Can only assign to elements of arrays');
|
|
1081
|
+
}
|
|
1082
|
+
if (typeof index !== 'number') {
|
|
1083
|
+
throw new Error('Array index must be a number');
|
|
1084
|
+
}
|
|
1085
|
+
if (index < 0 || index >= array.length) {
|
|
1086
|
+
throw new Error('Array index out of bounds');
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const currentValue = array[index];
|
|
1090
|
+
const newValue = this.performCompoundOperation(
|
|
1091
|
+
currentValue,
|
|
1092
|
+
expression.operator,
|
|
1093
|
+
rightValue
|
|
1094
|
+
);
|
|
1095
|
+
|
|
1096
|
+
array[index] = newValue;
|
|
1097
|
+
return newValue;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Evaluates compound property assignment
|
|
1102
|
+
* @param {Object} expression - Compound property assignment expression
|
|
1103
|
+
* @returns {any} Assignment result
|
|
1104
|
+
*/
|
|
1105
|
+
evaluateCompoundPropertyAssign(expression) {
|
|
1106
|
+
const object = this.evaluateExpression(expression.object);
|
|
1107
|
+
const rightValue = this.evaluateExpression(expression.value);
|
|
1108
|
+
|
|
1109
|
+
if (typeof object !== 'object' || object === null) {
|
|
1110
|
+
throw new Error('Can only assign to properties of objects');
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const currentValue = object[expression.name];
|
|
1114
|
+
const newValue = this.performCompoundOperation(
|
|
1115
|
+
currentValue,
|
|
1116
|
+
expression.operator,
|
|
1117
|
+
rightValue
|
|
1118
|
+
);
|
|
1119
|
+
|
|
1120
|
+
object[expression.name] = newValue;
|
|
1121
|
+
return newValue;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Performs compound operation
|
|
1126
|
+
* @param {any} left - Left operand
|
|
1127
|
+
* @param {string} operator - Compound operator
|
|
1128
|
+
* @param {any} right - Right operand
|
|
1129
|
+
* @returns {any} Operation result
|
|
1130
|
+
*/
|
|
1131
|
+
performCompoundOperation(left, operator, right) {
|
|
1132
|
+
switch (operator) {
|
|
1133
|
+
case 'PLUS_EQUAL':
|
|
1134
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
1135
|
+
return left + right;
|
|
1136
|
+
}
|
|
1137
|
+
if (typeof left === 'string' || typeof right === 'string') {
|
|
1138
|
+
return String(left) + String(right);
|
|
1139
|
+
}
|
|
1140
|
+
throw new Error('Cannot add non-numeric values');
|
|
1141
|
+
|
|
1142
|
+
case 'MINUS_EQUAL':
|
|
1143
|
+
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
1144
|
+
throw new Error('Can only subtract numbers');
|
|
1145
|
+
}
|
|
1146
|
+
return left - right;
|
|
1147
|
+
|
|
1148
|
+
case 'STAR_EQUAL':
|
|
1149
|
+
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
1150
|
+
throw new Error('Can only multiply numbers');
|
|
1151
|
+
}
|
|
1152
|
+
return left * right;
|
|
1153
|
+
|
|
1154
|
+
case 'SLASH_EQUAL':
|
|
1155
|
+
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
1156
|
+
throw new Error('Can only divide numbers');
|
|
1157
|
+
}
|
|
1158
|
+
if (right === 0) {
|
|
1159
|
+
throw new Error('Division by zero');
|
|
1160
|
+
}
|
|
1161
|
+
return left / right;
|
|
1162
|
+
|
|
1163
|
+
case 'PERCENT_EQUAL':
|
|
1164
|
+
if (typeof left !== 'number' || typeof right !== 'number') {
|
|
1165
|
+
throw new Error('Can only modulo numbers');
|
|
1166
|
+
}
|
|
1167
|
+
if (right === 0) {
|
|
1168
|
+
throw new Error('Modulo by zero');
|
|
1169
|
+
}
|
|
1170
|
+
return left % right;
|
|
1171
|
+
|
|
1172
|
+
default:
|
|
1173
|
+
throw new Error(`Unknown compound operator: ${operator}`);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Evaluates postfix expression (increment/decrement)
|
|
1179
|
+
* @param {Object} expression - Postfix expression
|
|
1180
|
+
* @returns {any} Postfix result
|
|
1181
|
+
*/
|
|
1182
|
+
evaluatePostfixExpression(expression) {
|
|
1183
|
+
const operand = this.evaluateExpression(expression.operand);
|
|
1184
|
+
|
|
1185
|
+
if (expression.operator === 'PLUS_PLUS') {
|
|
1186
|
+
if (typeof operand !== 'number') {
|
|
1187
|
+
throw new Error('Can only increment numbers');
|
|
1188
|
+
}
|
|
1189
|
+
const newValue = operand + 1;
|
|
1190
|
+
|
|
1191
|
+
// Update the variable if it's a variable reference
|
|
1192
|
+
if (expression.operand.type === 'Variable') {
|
|
1193
|
+
this.environment.assign(expression.operand.name, newValue);
|
|
1194
|
+
} else if (expression.operand.type === 'PropertyAccess') {
|
|
1195
|
+
const object = this.evaluateExpression(expression.operand.object);
|
|
1196
|
+
if (typeof object !== 'object' || object === null) {
|
|
1197
|
+
throw new Error('Can only increment properties of objects');
|
|
1198
|
+
}
|
|
1199
|
+
object[expression.operand.name] = newValue;
|
|
1200
|
+
} else if (expression.operand.type === 'ArrayAccess') {
|
|
1201
|
+
const array = this.evaluateExpression(expression.operand.array);
|
|
1202
|
+
const index = this.evaluateExpression(expression.operand.index);
|
|
1203
|
+
if (!Array.isArray(array)) {
|
|
1204
|
+
throw new Error('Can only increment elements of arrays');
|
|
1205
|
+
}
|
|
1206
|
+
if (typeof index !== 'number') {
|
|
1207
|
+
throw new Error('Array index must be a number');
|
|
1208
|
+
}
|
|
1209
|
+
if (index < 0 || index >= array.length) {
|
|
1210
|
+
throw new Error('Array index out of bounds');
|
|
1211
|
+
}
|
|
1212
|
+
array[index] = newValue;
|
|
1213
|
+
} else {
|
|
1214
|
+
throw new Error('Invalid increment target');
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return operand; // Return the original value (postfix behavior)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (expression.operator === 'MINUS_MINUS') {
|
|
1221
|
+
if (typeof operand !== 'number') {
|
|
1222
|
+
throw new Error('Can only decrement numbers');
|
|
1223
|
+
}
|
|
1224
|
+
const newValue = operand - 1;
|
|
1225
|
+
|
|
1226
|
+
// Update the variable if it's a variable reference
|
|
1227
|
+
if (expression.operand.type === 'Variable') {
|
|
1228
|
+
this.environment.assign(expression.operand.name, newValue);
|
|
1229
|
+
} else if (expression.operand.type === 'PropertyAccess') {
|
|
1230
|
+
const object = this.evaluateExpression(expression.operand.object);
|
|
1231
|
+
if (typeof object !== 'object' || object === null) {
|
|
1232
|
+
throw new Error('Can only decrement properties of objects');
|
|
1233
|
+
}
|
|
1234
|
+
object[expression.operand.name] = newValue;
|
|
1235
|
+
} else if (expression.operand.type === 'ArrayAccess') {
|
|
1236
|
+
const array = this.evaluateExpression(expression.operand.array);
|
|
1237
|
+
const index = this.evaluateExpression(expression.operand.index);
|
|
1238
|
+
if (!Array.isArray(array)) {
|
|
1239
|
+
throw new Error('Can only decrement elements of arrays');
|
|
1240
|
+
}
|
|
1241
|
+
if (typeof index !== 'number') {
|
|
1242
|
+
throw new Error('Array index must be a number');
|
|
1243
|
+
}
|
|
1244
|
+
if (index < 0 || index >= array.length) {
|
|
1245
|
+
throw new Error('Array index out of bounds');
|
|
1246
|
+
}
|
|
1247
|
+
array[index] = newValue;
|
|
1248
|
+
} else {
|
|
1249
|
+
throw new Error('Invalid decrement target');
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
return operand; // Return the original value (postfix behavior)
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
throw new Error(`Unknown postfix operator: ${expression.operator}`);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Converts a value to string for display
|
|
1260
|
+
* @param {any} value - Value to convert
|
|
1261
|
+
* @returns {string} String representation
|
|
1262
|
+
*/
|
|
1263
|
+
stringify(value) {
|
|
1264
|
+
if (value === null) return 'null';
|
|
1265
|
+
if (value === undefined) return 'undefined';
|
|
1266
|
+
if (typeof value === 'string') return value;
|
|
1267
|
+
if (Array.isArray(value)) {
|
|
1268
|
+
return '[' + value.map(v => this.stringify(v)).join(', ') + ']';
|
|
1269
|
+
}
|
|
1270
|
+
return value.toString();
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Executes a try-catch statement
|
|
1275
|
+
* @param {Object} statement - Try-catch statement to execute
|
|
1276
|
+
*/
|
|
1277
|
+
executeTryCatch(statement) {
|
|
1278
|
+
try {
|
|
1279
|
+
// Execute the try block
|
|
1280
|
+
this.executeBlock(statement.tryBlock);
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
// Check if it's a control flow exception (break/continue)
|
|
1283
|
+
if (
|
|
1284
|
+
error instanceof BreakException ||
|
|
1285
|
+
error instanceof ContinueException
|
|
1286
|
+
) {
|
|
1287
|
+
throw error; // Re-throw control flow exceptions
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Store the error in the environment for the catch block
|
|
1291
|
+
this.environment.define(statement.errorVariable, error.message);
|
|
1292
|
+
|
|
1293
|
+
// Execute the catch block
|
|
1294
|
+
this.executeBlock(statement.catchBlock);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* Class to handle variable environment
|
|
1301
|
+
*/
|
|
1302
|
+
class Environment {
|
|
1303
|
+
constructor(enclosing = null) {
|
|
1304
|
+
this.values = {};
|
|
1305
|
+
this.enclosing = enclosing;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Defines a variable in the environment
|
|
1310
|
+
* @param {string} name - Variable name
|
|
1311
|
+
* @param {any} value - Variable value
|
|
1312
|
+
*/
|
|
1313
|
+
define(name, value) {
|
|
1314
|
+
this.values[name] = value;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Assigns a value to an existing variable
|
|
1319
|
+
* @param {string} name - Variable name
|
|
1320
|
+
* @param {any} value - Value to assign
|
|
1321
|
+
*/
|
|
1322
|
+
assign(name, value) {
|
|
1323
|
+
if (name in this.values) {
|
|
1324
|
+
this.values[name] = value;
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
if (this.enclosing !== null) {
|
|
1329
|
+
this.enclosing.assign(name, value);
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
throw new Error(`Undefined variable: ${name}`);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Gets the value of a variable
|
|
1338
|
+
* @param {string} name - Variable name
|
|
1339
|
+
* @returns {any} Variable value
|
|
1340
|
+
*/
|
|
1341
|
+
get(name) {
|
|
1342
|
+
if (name in this.values) {
|
|
1343
|
+
return this.values[name];
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (this.enclosing !== null) {
|
|
1347
|
+
return this.enclosing.get(name);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
throw new Error(`Undefined variable: ${name}`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Exception class for return statements
|
|
1356
|
+
*/
|
|
1357
|
+
class ReturnException {
|
|
1358
|
+
constructor(value) {
|
|
1359
|
+
this.value = value;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* Exception class for break statements
|
|
1365
|
+
*/
|
|
1366
|
+
class BreakException {
|
|
1367
|
+
constructor() {
|
|
1368
|
+
this.type = 'break';
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Exception class for continue statements
|
|
1374
|
+
*/
|
|
1375
|
+
class ContinueException {
|
|
1376
|
+
constructor() {
|
|
1377
|
+
this.type = 'continue';
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Exception class for try-catch errors
|
|
1383
|
+
*/
|
|
1384
|
+
class TryCatchException {
|
|
1385
|
+
constructor(message) {
|
|
1386
|
+
this.type = 'try-catch';
|
|
1387
|
+
this.message = message;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
module.exports = Evaluator;
|