codexivo 0.2.0 → 0.2.3
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/CHANGELOG.md +1 -1
- package/README.md +26 -25
- package/dist/index.js +151 -9
- package/dist/index.js.map +22 -12
- package/dist/worker.js +153 -0
- package/dist/worker.js.map +25 -0
- package/docs/README.md +2 -0
- package/docs/arquitectura.md +86 -4
- package/docs/especificacion-v1.md +192 -86
- package/docs/guia-aprender-desde-el-codigo.md +5 -5
- package/docs/integracion.md +104 -0
- package/docs/lenguaje.md +435 -60
- package/docs/modo-mundo.md +128 -0
- package/docs/recetario.md +63 -63
- package/package.json +11 -20
- package/src/catalog.ts +256 -0
- package/src/challenge.ts +138 -0
- package/src/engine.ts +7 -0
- package/src/error-codes.ts +427 -0
- package/src/flow.ts +385 -0
- package/src/index.ts +53 -1
- package/src/printer.ts +133 -0
- package/src/runtime/builtins.ts +1358 -65
- package/src/runtime/compiler.ts +278 -31
- package/src/runtime/costs.ts +96 -0
- package/src/runtime/machine.ts +722 -48
- package/src/runtime/prelude.ts +230 -0
- package/src/runtime/values.ts +119 -18
- package/src/sandbox-host.ts +310 -0
- package/src/sandbox.ts +95 -0
- package/src/syntax/ast.ts +120 -6
- package/src/syntax/lexer.ts +39 -5
- package/src/syntax/parser.ts +496 -72
- package/src/syntax/token.ts +49 -1
- package/src/worker.ts +18 -0
- package/src/world.ts +518 -0
- package/bin/codexivo.exe +0 -1
- package/postinstall.mjs +0 -38
package/src/syntax/parser.ts
CHANGED
|
@@ -31,6 +31,7 @@ enum Precedence {
|
|
|
31
31
|
SUM,
|
|
32
32
|
PRODUCT,
|
|
33
33
|
UNARY,
|
|
34
|
+
POWER,
|
|
34
35
|
CALL,
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -47,8 +48,13 @@ const INFIX_PRECEDENCE: Partial<Record<TokenType, Precedence>> = {
|
|
|
47
48
|
[TokenType.MINUS]: Precedence.SUM,
|
|
48
49
|
[TokenType.STAR]: Precedence.PRODUCT,
|
|
49
50
|
[TokenType.SLASH]: Precedence.PRODUCT,
|
|
51
|
+
[TokenType.PERCENT]: Precedence.PRODUCT,
|
|
52
|
+
[TokenType.MOD]: Precedence.PRODUCT,
|
|
53
|
+
[TokenType.DIV]: Precedence.PRODUCT,
|
|
54
|
+
[TokenType.CARET]: Precedence.POWER,
|
|
50
55
|
[TokenType.LPAREN]: Precedence.CALL,
|
|
51
56
|
[TokenType.LBRACKET]: Precedence.CALL,
|
|
57
|
+
[TokenType.DOT]: Precedence.CALL,
|
|
52
58
|
};
|
|
53
59
|
|
|
54
60
|
const BINARY_OPERATORS: Partial<Record<TokenType, ast.BinaryOperator>> = {
|
|
@@ -56,6 +62,10 @@ const BINARY_OPERATORS: Partial<Record<TokenType, ast.BinaryOperator>> = {
|
|
|
56
62
|
[TokenType.MINUS]: '-',
|
|
57
63
|
[TokenType.STAR]: '*',
|
|
58
64
|
[TokenType.SLASH]: '/',
|
|
65
|
+
[TokenType.PERCENT]: 'mod',
|
|
66
|
+
[TokenType.MOD]: 'mod',
|
|
67
|
+
[TokenType.DIV]: 'div',
|
|
68
|
+
[TokenType.CARET]: '^',
|
|
59
69
|
[TokenType.EQ]: '==',
|
|
60
70
|
[TokenType.NEQ]: '!=',
|
|
61
71
|
[TokenType.LT]: '<',
|
|
@@ -70,10 +80,23 @@ const ASSIGNMENT_OPERATORS: Partial<Record<TokenType, ast.AssignmentOperator>> =
|
|
|
70
80
|
[TokenType.MINUS_ASSIGN]: '-=',
|
|
71
81
|
[TokenType.STAR_ASSIGN]: '*=',
|
|
72
82
|
[TokenType.SLASH_ASSIGN]: '/=',
|
|
83
|
+
[TokenType.PERCENT_ASSIGN]: '%=',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const INCREMENT_OPERATORS: Partial<Record<TokenType, ast.IncrementOperator>> = {
|
|
87
|
+
[TokenType.INCREMENT]: '++',
|
|
88
|
+
[TokenType.DECREMENT]: '--',
|
|
73
89
|
};
|
|
74
90
|
|
|
75
91
|
const STATEMENT_KEYWORDS = new Set([
|
|
76
92
|
TokenType.LET,
|
|
93
|
+
TokenType.DOMAIN,
|
|
94
|
+
TokenType.CONST,
|
|
95
|
+
TokenType.REPEAT,
|
|
96
|
+
TokenType.FOR_EACH,
|
|
97
|
+
TokenType.BREAK,
|
|
98
|
+
TokenType.CONTINUE,
|
|
99
|
+
TokenType.FUNCTION,
|
|
77
100
|
TokenType.RETURN,
|
|
78
101
|
TokenType.IF,
|
|
79
102
|
TokenType.WHILE,
|
|
@@ -81,10 +104,40 @@ const STATEMENT_KEYWORDS = new Set([
|
|
|
81
104
|
TokenType.FOR,
|
|
82
105
|
]);
|
|
83
106
|
|
|
107
|
+
/** Lo único que puede ir dentro de un `dominio`. */
|
|
108
|
+
const DOMAIN_MEMBERS = new Set<ast.Statement['kind']>(['VariableDeclaration', 'FunctionDeclaration', 'Domain']);
|
|
109
|
+
|
|
110
|
+
/** Cómo empieza una sentencia, para nombrarla en un mensaje. */
|
|
111
|
+
const printStart = (statement: ast.Statement): string | undefined => {
|
|
112
|
+
switch (statement.kind) {
|
|
113
|
+
case 'If':
|
|
114
|
+
return 'si';
|
|
115
|
+
case 'While':
|
|
116
|
+
return 'mientras';
|
|
117
|
+
case 'DoUntil':
|
|
118
|
+
return 'hacer';
|
|
119
|
+
case 'For':
|
|
120
|
+
return 'para';
|
|
121
|
+
case 'ForEach':
|
|
122
|
+
return 'para_cada';
|
|
123
|
+
case 'Repeat':
|
|
124
|
+
return 'repetir';
|
|
125
|
+
case 'Return':
|
|
126
|
+
return 'regresa';
|
|
127
|
+
case 'Break':
|
|
128
|
+
return 'romper';
|
|
129
|
+
case 'Continue':
|
|
130
|
+
return 'continuar';
|
|
131
|
+
case 'Block':
|
|
132
|
+
return '{';
|
|
133
|
+
default:
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
84
138
|
const KEYWORD_LIKE_TYPOS: Readonly<Record<string, string>> = {
|
|
85
139
|
sino: 'En Codexivo se escribe `si_no`, con guion bajo.',
|
|
86
140
|
pero: 'En Codexivo se escribe `pero_si`, con guion bajo.',
|
|
87
|
-
hasta: 'En Codexivo se escribe `hasta_que`, con guion bajo.',
|
|
88
141
|
};
|
|
89
142
|
|
|
90
143
|
const MAX_ERRORS = 50;
|
|
@@ -98,6 +151,13 @@ class Parser {
|
|
|
98
151
|
private position = 0;
|
|
99
152
|
private nextId = 1;
|
|
100
153
|
private depth = 0;
|
|
154
|
+
/**
|
|
155
|
+
* Paréntesis, corchetes o llaves abiertos alrededor de la expresión actual. Dentro de ellos una expresión puede
|
|
156
|
+
* seguir en la línea siguiente; fuera, un salto de línea la termina (salvo después de un operador).
|
|
157
|
+
*/
|
|
158
|
+
private grouping = 0;
|
|
159
|
+
/** Bucles abiertos alrededor de la sentencia actual, sin contar los de afuera de un procedimiento. */
|
|
160
|
+
private loopDepth = 0;
|
|
101
161
|
|
|
102
162
|
constructor(private readonly tokens: Token[]) {}
|
|
103
163
|
|
|
@@ -163,7 +223,7 @@ class Parser {
|
|
|
163
223
|
const found = this.current;
|
|
164
224
|
let finalHint = hint;
|
|
165
225
|
if (type === TokenType.RPAREN && found.type === TokenType.ASSIGN) {
|
|
166
|
-
finalHint = 'Para comparar se usa
|
|
226
|
+
finalHint = 'Para comparar se usa `es_igual_a` (o `==`); un solo `=` sirve para asignar.';
|
|
167
227
|
}
|
|
168
228
|
this.error(
|
|
169
229
|
'TOKEN_ESPERADO',
|
|
@@ -204,6 +264,18 @@ class Parser {
|
|
|
204
264
|
// ── sentencias ─────────────────────────────────────────────────────────────
|
|
205
265
|
|
|
206
266
|
private parseStatements(terminator: TokenType.EOF | TokenType.RBRACE): ast.Statement[] {
|
|
267
|
+
// Un bloque dentro de un paréntesis (el cuerpo de un procedimiento que se pasa como argumento) vuelve a
|
|
268
|
+
// terminar sus sentencias con el salto de línea.
|
|
269
|
+
const grouping = this.grouping;
|
|
270
|
+
this.grouping = 0;
|
|
271
|
+
try {
|
|
272
|
+
return this.parseStatementList(terminator);
|
|
273
|
+
} finally {
|
|
274
|
+
this.grouping = grouping;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private parseStatementList(terminator: TokenType.EOF | TokenType.RBRACE): ast.Statement[] {
|
|
207
279
|
const statements: ast.Statement[] = [];
|
|
208
280
|
let recovering = false;
|
|
209
281
|
while (!this.check(terminator) && !this.check(TokenType.EOF)) {
|
|
@@ -226,9 +298,38 @@ class Parser {
|
|
|
226
298
|
this.synchronize(failedAt);
|
|
227
299
|
}
|
|
228
300
|
}
|
|
301
|
+
this.checkDuplicateProcedures(statements);
|
|
229
302
|
return statements;
|
|
230
303
|
}
|
|
231
304
|
|
|
305
|
+
/** Un procedimiento con nombre existe desde el inicio de su bloque, así que su nombre no se puede repetir ahí. */
|
|
306
|
+
private checkDuplicateProcedures(statements: ast.Statement[]): void {
|
|
307
|
+
const procedures = new Set(
|
|
308
|
+
statements.flatMap(statement => (statement.kind === 'FunctionDeclaration' ? [statement.name.name] : [])),
|
|
309
|
+
);
|
|
310
|
+
if (procedures.size === 0) return;
|
|
311
|
+
const seen = new Set<string>();
|
|
312
|
+
for (const statement of statements) {
|
|
313
|
+
if (statement.kind !== 'FunctionDeclaration' && statement.kind !== 'VariableDeclaration') continue;
|
|
314
|
+
const { name } = statement;
|
|
315
|
+
if (!procedures.has(name.name)) continue;
|
|
316
|
+
if (statement.kind === 'FunctionDeclaration' && !seen.has(name.name)) {
|
|
317
|
+
seen.add(name.name);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (this.errors.length >= MAX_ERRORS) return;
|
|
321
|
+
this.errors.push(
|
|
322
|
+
new CodexivoError({
|
|
323
|
+
kind: 'sintaxis',
|
|
324
|
+
code: 'NOMBRE_REPETIDO',
|
|
325
|
+
message: `Ya hay un procedimiento llamado \`${name.name}\` en este bloque.`,
|
|
326
|
+
hint: 'Un procedimiento con nombre existe desde el inicio de su bloque; cambia uno de los dos nombres.',
|
|
327
|
+
span: name.span,
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
232
333
|
private parseStatement(): ast.Statement {
|
|
233
334
|
const token = this.current;
|
|
234
335
|
switch (token.type) {
|
|
@@ -236,6 +337,7 @@ class Parser {
|
|
|
236
337
|
this.advance();
|
|
237
338
|
return this.parseStatementAfterStraySemicolon();
|
|
238
339
|
case TokenType.LET:
|
|
340
|
+
case TokenType.CONST:
|
|
239
341
|
return this.endSimpleStatement(this.parseVariableDeclaration());
|
|
240
342
|
case TokenType.RETURN:
|
|
241
343
|
return this.endSimpleStatement(this.parseReturn());
|
|
@@ -247,7 +349,32 @@ class Parser {
|
|
|
247
349
|
return this.endSimpleStatement(this.parseDoUntil());
|
|
248
350
|
case TokenType.FOR:
|
|
249
351
|
return this.endCompoundStatement(this.parseFor());
|
|
352
|
+
case TokenType.REPEAT:
|
|
353
|
+
return this.endCompoundStatement(this.parseRepeat());
|
|
354
|
+
case TokenType.FOR_EACH:
|
|
355
|
+
return this.endCompoundStatement(this.parseForEach());
|
|
356
|
+
case TokenType.DOMAIN:
|
|
357
|
+
return this.endCompoundStatement(this.parseDomain());
|
|
358
|
+
case TokenType.BREAK:
|
|
359
|
+
case TokenType.CONTINUE:
|
|
360
|
+
return this.endSimpleStatement(this.parseLoopJump());
|
|
361
|
+
case TokenType.IN:
|
|
362
|
+
this.error('EN_SIN_PARA_CADA', '`en` solo se usa dentro de `para_cada … en …`.', token.span);
|
|
363
|
+
case TokenType.FUNCTION:
|
|
364
|
+
if (this.peek().type === TokenType.IDENT) return this.endCompoundStatement(this.parseFunctionDeclaration());
|
|
365
|
+
return this.endSimpleStatement(this.parseSimpleStatement());
|
|
250
366
|
case TokenType.LBRACE:
|
|
367
|
+
if (
|
|
368
|
+
(this.peek().type === TokenType.STRING || this.peek().type === TokenType.NUMBER) &&
|
|
369
|
+
this.peek(2).type === TokenType.COLON
|
|
370
|
+
) {
|
|
371
|
+
this.error(
|
|
372
|
+
'DICCIONARIO_COMO_SENTENCIA',
|
|
373
|
+
'Al inicio de una sentencia, `{` abre un bloque, no un diccionario.',
|
|
374
|
+
token.span,
|
|
375
|
+
'Guarda el diccionario en una variable: `variable d = { "clave": valor }`.',
|
|
376
|
+
);
|
|
377
|
+
}
|
|
251
378
|
return this.endCompoundStatement(this.parseBlock('para abrir el bloque'));
|
|
252
379
|
case TokenType.ELSE:
|
|
253
380
|
case TokenType.ELSEIF:
|
|
@@ -267,13 +394,10 @@ class Parser {
|
|
|
267
394
|
) {
|
|
268
395
|
this.error('PALABRA_DESCONOCIDA', `\`${token.lexeme}\` no es una palabra de Codexivo.`, token.span, typoHint);
|
|
269
396
|
}
|
|
270
|
-
|
|
271
|
-
return this.endSimpleStatement(this.parseAssignment());
|
|
272
|
-
}
|
|
273
|
-
return this.endSimpleStatement(this.parseExpressionStatement());
|
|
397
|
+
return this.endSimpleStatement(this.parseSimpleStatement());
|
|
274
398
|
}
|
|
275
399
|
default:
|
|
276
|
-
return this.endSimpleStatement(this.
|
|
400
|
+
return this.endSimpleStatement(this.parseSimpleStatement());
|
|
277
401
|
}
|
|
278
402
|
}
|
|
279
403
|
|
|
@@ -306,16 +430,26 @@ class Parser {
|
|
|
306
430
|
}
|
|
307
431
|
|
|
308
432
|
private parseVariableDeclaration(): ast.VariableDeclaration {
|
|
309
|
-
const
|
|
310
|
-
const
|
|
433
|
+
const keyword = this.advance(); // variable / constante
|
|
434
|
+
const constant = keyword.type === TokenType.CONST;
|
|
435
|
+
const name = this.parseBindingName(`después de \`${keyword.lexeme}\``);
|
|
311
436
|
this.expect(
|
|
312
437
|
TokenType.ASSIGN,
|
|
313
|
-
`después de
|
|
314
|
-
|
|
438
|
+
`después de \`${keyword.lexeme} ${name.name}\``,
|
|
439
|
+
constant
|
|
440
|
+
? `Toda constante necesita su valor desde el principio: \`constante ${name.name} = 10\`.`
|
|
441
|
+
: `Toda variable empieza con un valor: \`variable ${name.name} = 0\`.`,
|
|
315
442
|
);
|
|
316
443
|
const value = this.parseExpression();
|
|
317
444
|
if (value.kind === 'FunctionLiteral' && !value.name) value.name = name.name;
|
|
318
|
-
return {
|
|
445
|
+
return {
|
|
446
|
+
kind: 'VariableDeclaration',
|
|
447
|
+
id: this.id(),
|
|
448
|
+
constant,
|
|
449
|
+
name,
|
|
450
|
+
value,
|
|
451
|
+
span: this.spanFrom(keyword.span.start),
|
|
452
|
+
};
|
|
319
453
|
}
|
|
320
454
|
|
|
321
455
|
private parseBindingName(context: string): ast.Identifier {
|
|
@@ -323,6 +457,8 @@ class Parser {
|
|
|
323
457
|
if (token.type !== TokenType.IDENT) {
|
|
324
458
|
this.failSilentlyOnIllegal();
|
|
325
459
|
if (token.lexeme && /^[a-z_]+$/.test(token.lexeme) && token.type !== TokenType.EOF) {
|
|
460
|
+
// Se salta la palabra para que, al recuperarse, no empiece otra sentencia (`variable dominio = 1`).
|
|
461
|
+
this.advance();
|
|
326
462
|
this.error(
|
|
327
463
|
'PALABRA_RESERVADA',
|
|
328
464
|
`\`${token.lexeme}\` es una palabra reservada y no se puede usar como nombre.`,
|
|
@@ -340,34 +476,40 @@ class Parser {
|
|
|
340
476
|
return { kind: 'Identifier', id: this.id(), name: token.lexeme, span: token.span };
|
|
341
477
|
}
|
|
342
478
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const target: ast.Identifier = {
|
|
346
|
-
kind: 'Identifier',
|
|
347
|
-
id: this.id(),
|
|
348
|
-
name: targetToken.lexeme,
|
|
349
|
-
span: targetToken.span,
|
|
350
|
-
};
|
|
351
|
-
const operator = ASSIGNMENT_OPERATORS[this.advance().type]!;
|
|
352
|
-
const value = this.parseExpression();
|
|
353
|
-
if (value.kind === 'FunctionLiteral' && !value.name) value.name = target.name;
|
|
354
|
-
return { kind: 'Assignment', id: this.id(), target, operator, value, span: this.spanFrom(targetToken.span.start) };
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
private parseExpressionStatement(): ast.ExpressionStatement {
|
|
479
|
+
/** Asignación (`x = …`, `lista[i] += …`), incremento (`i++`) o expresión. */
|
|
480
|
+
private parseSimpleStatement(): ast.Assignment | ast.Increment | ast.ExpressionStatement {
|
|
358
481
|
const start = this.current.span.start;
|
|
359
|
-
const expression = this.parseExpression();
|
|
360
|
-
|
|
482
|
+
const expression = this.parseExpression(Precedence.LOWEST, true);
|
|
483
|
+
const operator = ASSIGNMENT_OPERATORS[this.current.type];
|
|
484
|
+
const increment = INCREMENT_OPERATORS[this.current.type];
|
|
485
|
+
if (!operator && !increment) {
|
|
486
|
+
return { kind: 'ExpressionStatement', id: this.id(), expression, span: this.spanFrom(start) };
|
|
487
|
+
}
|
|
488
|
+
if (expression.kind !== 'Identifier' && expression.kind !== 'Index') {
|
|
361
489
|
this.error(
|
|
362
490
|
'ASIGNACIÓN_INVÁLIDA',
|
|
363
|
-
|
|
491
|
+
increment
|
|
492
|
+
? `\`${this.current.lexeme}\` solo funciona con una variable o un elemento, como \`i${this.current.lexeme}\` o \`lista[0]${this.current.lexeme}\`.`
|
|
493
|
+
: 'Solo se le puede asignar un valor a una variable o a un elemento, como `x = 1` o `lista[0] = 1`.',
|
|
364
494
|
this.current.span,
|
|
365
|
-
expression.kind === 'Index'
|
|
366
|
-
? 'Cambiar un elemento de una lista con `lista[i] = valor` llegará en la siguiente versión.'
|
|
367
|
-
: undefined,
|
|
368
495
|
);
|
|
369
496
|
}
|
|
370
|
-
|
|
497
|
+
this.advance();
|
|
498
|
+
if (increment) {
|
|
499
|
+
return { kind: 'Increment', id: this.id(), target: expression, operator: increment, span: this.spanFrom(start) };
|
|
500
|
+
}
|
|
501
|
+
const value = this.parseExpression();
|
|
502
|
+
if (value.kind === 'FunctionLiteral' && !value.name && expression.kind === 'Identifier') {
|
|
503
|
+
value.name = expression.name;
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
kind: 'Assignment',
|
|
507
|
+
id: this.id(),
|
|
508
|
+
target: expression,
|
|
509
|
+
operator: operator!,
|
|
510
|
+
value,
|
|
511
|
+
span: this.spanFrom(start),
|
|
512
|
+
};
|
|
371
513
|
}
|
|
372
514
|
|
|
373
515
|
private parseReturn(): ast.Return {
|
|
@@ -395,13 +537,23 @@ class Parser {
|
|
|
395
537
|
return { kind: 'Block', id: this.id(), statements, span: this.spanFrom(open.span.start) };
|
|
396
538
|
}
|
|
397
539
|
|
|
540
|
+
/** Analiza `parse` dentro de paréntesis, corchetes o llaves, donde los saltos de línea no terminan nada. */
|
|
541
|
+
private grouped<T>(parse: () => T): T {
|
|
542
|
+
this.grouping += 1;
|
|
543
|
+
try {
|
|
544
|
+
return parse();
|
|
545
|
+
} finally {
|
|
546
|
+
this.grouping -= 1;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
398
550
|
private parseCondition(keyword: string): ast.Expression {
|
|
399
551
|
this.expect(
|
|
400
552
|
TokenType.LPAREN,
|
|
401
553
|
`después de \`${keyword}\``,
|
|
402
554
|
`La condición va entre paréntesis: \`${keyword} (x > 0)\`.`,
|
|
403
555
|
);
|
|
404
|
-
const condition = this.parseExpression();
|
|
556
|
+
const condition = this.grouped(() => this.parseExpression());
|
|
405
557
|
this.expect(TokenType.RPAREN, `para cerrar la condición de \`${keyword}\``);
|
|
406
558
|
return condition;
|
|
407
559
|
}
|
|
@@ -436,64 +588,237 @@ class Parser {
|
|
|
436
588
|
private parseWhile(): ast.While {
|
|
437
589
|
const start = this.advance().span.start;
|
|
438
590
|
const condition = this.parseCondition('mientras');
|
|
439
|
-
const body = this.
|
|
591
|
+
const body = this.parseLoopBody('después de la condición de `mientras`');
|
|
440
592
|
return { kind: 'While', id: this.id(), condition, body, span: this.spanFrom(start) };
|
|
441
593
|
}
|
|
442
594
|
|
|
443
595
|
private parseDoUntil(): ast.DoUntil {
|
|
444
596
|
const start = this.advance().span.start;
|
|
445
|
-
const body = this.
|
|
597
|
+
const body = this.parseLoopBody('después de `hacer`');
|
|
446
598
|
this.expect(
|
|
447
599
|
TokenType.UNTIL,
|
|
448
600
|
'después del bloque de `hacer`',
|
|
449
|
-
'La forma completa es `hacer { … } hasta_que (condición)
|
|
601
|
+
'La forma completa es `hacer { … } hasta_que (condición)`.',
|
|
450
602
|
);
|
|
451
603
|
const condition = this.parseCondition('hasta_que');
|
|
452
604
|
return { kind: 'DoUntil', id: this.id(), body, condition, span: this.spanFrom(start) };
|
|
453
605
|
}
|
|
454
606
|
|
|
455
|
-
private parseFor(): ast.For {
|
|
607
|
+
private parseFor(): ast.For | ast.ForRange {
|
|
456
608
|
const start = this.advance().span.start;
|
|
609
|
+
// `para i desde …` cuenta; `para (…; …; …)` es la forma clásica.
|
|
610
|
+
if (!this.check(TokenType.LPAREN)) return this.parseForRange(start);
|
|
457
611
|
this.expect(
|
|
458
612
|
TokenType.LPAREN,
|
|
459
613
|
'después de `para`',
|
|
460
614
|
'La forma completa es `para (variable i = 0; i < 10; i += 1) { … }`.',
|
|
461
615
|
);
|
|
462
616
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (this.check(TokenType.
|
|
466
|
-
|
|
467
|
-
|
|
617
|
+
const { init, condition, update } = this.grouped(() => {
|
|
618
|
+
let init: ast.For['init'];
|
|
619
|
+
if (!this.check(TokenType.SEMICOLON)) {
|
|
620
|
+
if (this.check(TokenType.LET) || this.check(TokenType.CONST)) init = this.parseVariableDeclaration();
|
|
621
|
+
else init = this.parseSimpleStatement();
|
|
622
|
+
}
|
|
623
|
+
this.expect(TokenType.SEMICOLON, 'después del inicio del `para`');
|
|
624
|
+
|
|
625
|
+
const condition = this.check(TokenType.SEMICOLON) ? undefined : this.parseExpression();
|
|
626
|
+
this.expect(TokenType.SEMICOLON, 'después de la condición del `para`');
|
|
627
|
+
|
|
628
|
+
let update: ast.For['update'];
|
|
629
|
+
if (!this.check(TokenType.RPAREN)) update = this.parseSimpleStatement();
|
|
630
|
+
this.expect(TokenType.RPAREN, 'para cerrar el `para`');
|
|
631
|
+
return { init, condition, update };
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
const body = this.parseLoopBody('después de `para (…)`');
|
|
635
|
+
return { kind: 'For', id: this.id(), init, condition, update, body, span: this.spanFrom(start) };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private parseForRange(start: Position): ast.ForRange {
|
|
639
|
+
const form = 'La forma completa es `para i desde 1 hasta 10 { … }`.';
|
|
640
|
+
if (this.check(TokenType.LET)) {
|
|
641
|
+
// Se salta `variable` para no reportar también la declaración incompleta.
|
|
642
|
+
this.advance();
|
|
643
|
+
this.error(
|
|
644
|
+
'TOKEN_ESPERADO',
|
|
645
|
+
'La variable de `para … desde … hasta` se escribe sin `variable`.',
|
|
646
|
+
this.current.span,
|
|
647
|
+
form,
|
|
648
|
+
);
|
|
468
649
|
}
|
|
469
|
-
this.
|
|
650
|
+
const variable = this.parseBindingName('después de `para`');
|
|
651
|
+
if (this.check(TokenType.ASSIGN)) {
|
|
652
|
+
this.error(
|
|
653
|
+
'TOKEN_ESPERADO',
|
|
654
|
+
`Para contar se escribe \`desde\`, no \`=\`: \`para ${variable.name} desde 1 hasta 10\`.`,
|
|
655
|
+
this.current.span,
|
|
656
|
+
'La forma clásica `para (variable i = 0; i < 10; i++)` va entre paréntesis.',
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
// Un `{` justo después de `desde`, `hasta` o `con_paso` es el cuerpo, no un diccionario: falta el valor.
|
|
660
|
+
const value = (after: string): ast.Expression => {
|
|
661
|
+
if (this.check(TokenType.LBRACE)) {
|
|
662
|
+
this.error('EXPRESIÓN_ESPERADA', `Falta un valor después de \`${after}\`.`, this.current.span, form);
|
|
663
|
+
}
|
|
664
|
+
return this.parseExpression();
|
|
665
|
+
};
|
|
666
|
+
this.expect(TokenType.FROM, `después de \`para ${variable.name}\``, form);
|
|
667
|
+
const from = value('desde');
|
|
668
|
+
this.expect(TokenType.TO, 'después del inicio del `para`', form);
|
|
669
|
+
const to = value('hasta');
|
|
670
|
+
const step = this.match(TokenType.STEP) ? value('con_paso') : undefined;
|
|
671
|
+
const body = this.parseLoopBody('después de `para … desde … hasta …`');
|
|
672
|
+
return { kind: 'ForRange', id: this.id(), variable, from, to, step, body, span: this.spanFrom(start) };
|
|
673
|
+
}
|
|
470
674
|
|
|
471
|
-
|
|
472
|
-
this.
|
|
675
|
+
private parseRepeat(): ast.Repeat {
|
|
676
|
+
const keyword = this.advance();
|
|
677
|
+
if (this.check(TokenType.TIMES) || this.check(TokenType.LBRACE)) {
|
|
678
|
+
this.error(
|
|
679
|
+
'REPETIR_SIN_CANTIDAD',
|
|
680
|
+
'Falta cuántas veces se repite.',
|
|
681
|
+
this.current.span,
|
|
682
|
+
'La forma completa es `repetir 4 veces { … }`.',
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
const count = this.parseExpression();
|
|
686
|
+
this.expect(
|
|
687
|
+
TokenType.TIMES,
|
|
688
|
+
'después de la cantidad de `repetir`',
|
|
689
|
+
'La forma completa es `repetir 4 veces { … }`.',
|
|
690
|
+
);
|
|
691
|
+
const body = this.parseLoopBody('después de `repetir … veces`');
|
|
692
|
+
return { kind: 'Repeat', id: this.id(), count, body, span: this.spanFrom(keyword.span.start) };
|
|
693
|
+
}
|
|
473
694
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
695
|
+
private parseDomain(): ast.Domain {
|
|
696
|
+
const keyword = this.advance();
|
|
697
|
+
const name = this.parseBindingName('después de `dominio`');
|
|
698
|
+
const open = this.expect(
|
|
699
|
+
TokenType.LBRACE,
|
|
700
|
+
`después de \`dominio ${name.name}\``,
|
|
701
|
+
'Lo que agrupa un dominio va entre llaves: `dominio calculadora { … }`.',
|
|
702
|
+
);
|
|
703
|
+
// Un dominio no está dentro de ningún bucle, aunque se escriba dentro de uno.
|
|
704
|
+
const loopDepth = this.loopDepth;
|
|
705
|
+
this.loopDepth = 0;
|
|
706
|
+
let statements: ast.Statement[];
|
|
707
|
+
try {
|
|
708
|
+
statements = this.parseStatements(TokenType.RBRACE);
|
|
709
|
+
} finally {
|
|
710
|
+
this.loopDepth = loopDepth;
|
|
478
711
|
}
|
|
479
|
-
this.
|
|
712
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
713
|
+
this.error(
|
|
714
|
+
'BLOQUE_SIN_CERRAR',
|
|
715
|
+
`Falta \`}\` para cerrar el dominio que empieza en la línea ${open.span.start.line}.`,
|
|
716
|
+
open.span,
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
this.advance();
|
|
720
|
+
for (const statement of statements) {
|
|
721
|
+
if (DOMAIN_MEMBERS.has(statement.kind) || this.errors.length >= MAX_ERRORS) continue;
|
|
722
|
+
this.errors.push(
|
|
723
|
+
new CodexivoError({
|
|
724
|
+
kind: 'sintaxis',
|
|
725
|
+
code: 'DOMINIO_SOLO_DECLARACIONES',
|
|
726
|
+
message: printStart(statement)
|
|
727
|
+
? `Un dominio solo agrupa declaraciones, y \`${printStart(statement)}\` no es una.`
|
|
728
|
+
: 'Un dominio solo agrupa declaraciones, y esta sentencia no es una.',
|
|
729
|
+
hint: 'Dentro de `dominio` van `variable`, `constante`, `procedimiento` u otro `dominio`. Lo demás va en un procedimiento o fuera del dominio.',
|
|
730
|
+
span: statement.span,
|
|
731
|
+
}),
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
const body: ast.Block = { kind: 'Block', id: this.id(), statements, span: this.spanFrom(open.span.start) };
|
|
735
|
+
return { kind: 'Domain', id: this.id(), name, body, span: this.spanFrom(keyword.span.start) };
|
|
736
|
+
}
|
|
480
737
|
|
|
481
|
-
|
|
482
|
-
|
|
738
|
+
private parseForEach(): ast.ForEach {
|
|
739
|
+
const keyword = this.advance();
|
|
740
|
+
const form = 'La forma completa es `para_cada fruta en frutas { … }`, sin paréntesis.';
|
|
741
|
+
if (this.check(TokenType.LPAREN)) {
|
|
742
|
+
this.error('TOKEN_ESPERADO', '`para_cada` no lleva paréntesis.', this.current.span, form);
|
|
743
|
+
}
|
|
744
|
+
const variable = this.parseBindingName('después de `para_cada`');
|
|
745
|
+
let value: ast.Identifier | undefined;
|
|
746
|
+
if (this.match(TokenType.COMMA)) {
|
|
747
|
+
value = this.parseBindingName(`después de \`para_cada ${variable.name},\``);
|
|
748
|
+
if (value.name === variable.name) {
|
|
749
|
+
this.error('PARÁMETRO_REPETIDO', `La variable \`${value.name}\` está repetida.`, value.span);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const names = value ? `${variable.name}, ${value.name}` : variable.name;
|
|
753
|
+
this.expect(TokenType.IN, `después de \`para_cada ${names}\``, form);
|
|
754
|
+
const iterable = this.parseExpression();
|
|
755
|
+
const body = this.parseLoopBody('después de `para_cada … en …`');
|
|
756
|
+
return {
|
|
757
|
+
kind: 'ForEach',
|
|
758
|
+
id: this.id(),
|
|
759
|
+
variable,
|
|
760
|
+
value,
|
|
761
|
+
iterable,
|
|
762
|
+
body,
|
|
763
|
+
span: this.spanFrom(keyword.span.start),
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** El cuerpo de un bucle: ahí sí se puede usar `romper` y `continuar`. */
|
|
768
|
+
private parseLoopBody(context: string): ast.Block {
|
|
769
|
+
this.loopDepth += 1;
|
|
770
|
+
try {
|
|
771
|
+
return this.parseBlock(context);
|
|
772
|
+
} finally {
|
|
773
|
+
this.loopDepth -= 1;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private parseLoopJump(): ast.Break | ast.Continue {
|
|
778
|
+
const token = this.advance();
|
|
779
|
+
const kind = token.type === TokenType.BREAK ? 'Break' : 'Continue';
|
|
780
|
+
if (this.loopDepth === 0) {
|
|
781
|
+
this.error(
|
|
782
|
+
kind === 'Break' ? 'ROMPER_FUERA_DE_BUCLE' : 'CONTINUAR_FUERA_DE_BUCLE',
|
|
783
|
+
`\`${token.lexeme}\` solo se puede usar dentro de un bucle.`,
|
|
784
|
+
token.span,
|
|
785
|
+
kind === 'Break'
|
|
786
|
+
? 'Para salir de un procedimiento usa `regresa`.'
|
|
787
|
+
: 'Dentro de un procedimiento, `continuar` no afecta al bucle desde donde se llamó.',
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
return { kind, id: this.id(), span: token.span };
|
|
483
791
|
}
|
|
484
792
|
|
|
485
793
|
// ── expresiones ────────────────────────────────────────────────────────────
|
|
486
794
|
|
|
487
|
-
|
|
795
|
+
/** `statement` indica que la expresión es una sentencia completa, donde `i++` sí está permitido. */
|
|
796
|
+
private parseExpression(precedence: Precedence = Precedence.LOWEST, statement = false): ast.Expression {
|
|
488
797
|
this.enter();
|
|
489
798
|
let left = this.parsePrefix();
|
|
490
799
|
while (precedence < (INFIX_PRECEDENCE[this.current.type] ?? Precedence.LOWEST)) {
|
|
800
|
+
// Sin `;`, una línea que empieza con `-`, `(` o `[` es otra sentencia, no la continuación de la anterior.
|
|
801
|
+
if (this.grouping === 0 && this.current.span.start.line > this.previous.span.end.line) break;
|
|
491
802
|
left = this.parseInfix(left);
|
|
492
803
|
}
|
|
804
|
+
if (!statement && INCREMENT_OPERATORS[this.current.type]) this.incrementInExpression(left);
|
|
493
805
|
this.leave();
|
|
494
806
|
return left;
|
|
495
807
|
}
|
|
496
808
|
|
|
809
|
+
private incrementInExpression(left?: ast.Expression): never {
|
|
810
|
+
const operator = this.current.lexeme;
|
|
811
|
+
const name = left?.kind === 'Identifier' ? left.name : 'i';
|
|
812
|
+
this.error(
|
|
813
|
+
'INCREMENTO_EN_EXPRESIÓN',
|
|
814
|
+
`\`${operator}\` es una sentencia: va sola, justo después del nombre de una variable.`,
|
|
815
|
+
this.current.span,
|
|
816
|
+
left?.kind === 'Identifier' || !left
|
|
817
|
+
? `Escribe \`${name}${operator}\` en su propia línea y después usa \`${name}\`.`
|
|
818
|
+
: `Escribe el \`${operator}\` en su propia línea, antes o después de usar el valor.`,
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
497
822
|
private parsePrefix(): ast.Expression {
|
|
498
823
|
const token = this.current;
|
|
499
824
|
switch (token.type) {
|
|
@@ -515,12 +840,14 @@ class Parser {
|
|
|
515
840
|
return { kind: 'Identifier', id: this.id(), name: token.lexeme, span: token.span };
|
|
516
841
|
case TokenType.LPAREN: {
|
|
517
842
|
this.advance();
|
|
518
|
-
const expression = this.parseExpression();
|
|
843
|
+
const expression = this.grouped(() => this.parseExpression());
|
|
519
844
|
this.expect(TokenType.RPAREN, 'para cerrar el paréntesis');
|
|
520
845
|
return expression;
|
|
521
846
|
}
|
|
522
847
|
case TokenType.LBRACKET:
|
|
523
848
|
return this.parseList();
|
|
849
|
+
case TokenType.LBRACE:
|
|
850
|
+
return this.parseDict();
|
|
524
851
|
case TokenType.MINUS: {
|
|
525
852
|
this.advance();
|
|
526
853
|
const operand = this.parseExpression(Precedence.UNARY);
|
|
@@ -533,7 +860,27 @@ class Parser {
|
|
|
533
860
|
return { kind: 'Unary', id: this.id(), operator: 'no', operand, span: this.spanFrom(token.span.start) };
|
|
534
861
|
}
|
|
535
862
|
case TokenType.FUNCTION:
|
|
536
|
-
|
|
863
|
+
if (this.peek().type === TokenType.IDENT) {
|
|
864
|
+
this.error(
|
|
865
|
+
'PROCEDIMIENTO_CON_NOMBRE',
|
|
866
|
+
'Un procedimiento con nombre es una sentencia; no puede ir dentro de una expresión.',
|
|
867
|
+
this.peek().span,
|
|
868
|
+
'Como valor, escríbelo sin nombre: `variable doble = procedimiento(x) { … }`.',
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
return this.parseFunction(this.advance().span.start);
|
|
872
|
+
case TokenType.INCREMENT:
|
|
873
|
+
case TokenType.DECREMENT:
|
|
874
|
+
if (this.peek().type === TokenType.IDENT) {
|
|
875
|
+
const name = this.peek().lexeme;
|
|
876
|
+
this.error(
|
|
877
|
+
'INCREMENTO_EN_EXPRESIÓN',
|
|
878
|
+
`En Codexivo \`${token.lexeme}\` va después del nombre de la variable.`,
|
|
879
|
+
{ start: token.span.start, end: this.peek().span.end },
|
|
880
|
+
`Escribe \`${name}${token.lexeme}\` como una sentencia propia.`,
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
this.incrementInExpression();
|
|
537
884
|
case TokenType.ILLEGAL:
|
|
538
885
|
throw new ParseFailure();
|
|
539
886
|
case TokenType.IF:
|
|
@@ -541,6 +888,12 @@ class Parser {
|
|
|
541
888
|
case TokenType.FOR:
|
|
542
889
|
case TokenType.DO:
|
|
543
890
|
case TokenType.LET:
|
|
891
|
+
case TokenType.CONST:
|
|
892
|
+
case TokenType.DOMAIN:
|
|
893
|
+
case TokenType.REPEAT:
|
|
894
|
+
case TokenType.FOR_EACH:
|
|
895
|
+
case TokenType.BREAK:
|
|
896
|
+
case TokenType.CONTINUE:
|
|
544
897
|
case TokenType.RETURN:
|
|
545
898
|
this.error(
|
|
546
899
|
'SENTENCIA_EN_EXPRESIÓN',
|
|
@@ -585,10 +938,33 @@ class Parser {
|
|
|
585
938
|
'Por ejemplo: `lista[0]`.',
|
|
586
939
|
);
|
|
587
940
|
}
|
|
588
|
-
const index = this.parseExpression();
|
|
941
|
+
const index = this.grouped(() => this.parseExpression());
|
|
589
942
|
this.expect(TokenType.RBRACKET, 'para cerrar el índice');
|
|
590
943
|
return { kind: 'Index', id: this.id(), object: left, index, span: this.spanFrom(start) };
|
|
591
944
|
}
|
|
945
|
+
case TokenType.DOT: {
|
|
946
|
+
this.advance();
|
|
947
|
+
const property = this.current;
|
|
948
|
+
if (property.type !== TokenType.IDENT) {
|
|
949
|
+
this.failSilentlyOnIllegal();
|
|
950
|
+
// Una palabra reservada como `si` no debe empezar otra sentencia al recuperarse del error.
|
|
951
|
+
if (property.type !== TokenType.EOF) this.advance();
|
|
952
|
+
this.error(
|
|
953
|
+
'CAMPO_ESPERADO',
|
|
954
|
+
`Después de \`.\` va el nombre de un campo, pero se encontró ${describeToken(property)}.`,
|
|
955
|
+
property.span,
|
|
956
|
+
'Con `.` solo se usan claves que son nombres válidos; para las demás escribe `d["clave"]`.',
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
this.advance();
|
|
960
|
+
const index: ast.StringLiteral = {
|
|
961
|
+
kind: 'StringLiteral',
|
|
962
|
+
id: this.id(),
|
|
963
|
+
value: property.lexeme,
|
|
964
|
+
span: property.span,
|
|
965
|
+
};
|
|
966
|
+
return { kind: 'Index', id: this.id(), object: left, index, dot: true, span: this.spanFrom(start) };
|
|
967
|
+
}
|
|
592
968
|
case TokenType.AND:
|
|
593
969
|
case TokenType.OR: {
|
|
594
970
|
this.advance();
|
|
@@ -600,7 +976,8 @@ class Parser {
|
|
|
600
976
|
default: {
|
|
601
977
|
this.advance();
|
|
602
978
|
const precedence = INFIX_PRECEDENCE[token.type]!;
|
|
603
|
-
|
|
979
|
+
// `^` es asociativo a la derecha: `2 ^ 3 ^ 2` es `2 ^ (3 ^ 2)`.
|
|
980
|
+
const right = this.parseExpression(precedence === Precedence.POWER ? precedence - 1 : precedence);
|
|
604
981
|
const operator = BINARY_OPERATORS[token.type]!;
|
|
605
982
|
if (precedence === Precedence.COMPARE && INFIX_PRECEDENCE[this.current.type] === Precedence.COMPARE) {
|
|
606
983
|
this.error(
|
|
@@ -621,28 +998,65 @@ class Parser {
|
|
|
621
998
|
return { kind: 'ListLiteral', id: this.id(), elements, span: this.spanFrom(start) };
|
|
622
999
|
}
|
|
623
1000
|
|
|
1001
|
+
private parseDict(): ast.DictLiteral {
|
|
1002
|
+
const open = this.advance();
|
|
1003
|
+
const entries: ast.DictEntry[] = [];
|
|
1004
|
+
const literalKeys = new Set<string | number>();
|
|
1005
|
+
while (!this.check(TokenType.RBRACE)) {
|
|
1006
|
+
const entryStart = this.current.span.start;
|
|
1007
|
+
if (this.check(TokenType.IDENT) && this.peek().type === TokenType.COLON) {
|
|
1008
|
+
this.error(
|
|
1009
|
+
'CLAVE_SIN_COMILLAS',
|
|
1010
|
+
`La clave \`${this.current.lexeme}\` necesita comillas.`,
|
|
1011
|
+
this.current.span,
|
|
1012
|
+
`Escribe \`"${this.current.lexeme}": …\`. Si querías usar el valor de una variable como clave, ponla entre paréntesis: \`(${this.current.lexeme}): …\`.`,
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
const key = this.grouped(() => this.parseExpression());
|
|
1016
|
+
this.expect(TokenType.COLON, 'después de la clave', 'Cada elemento de un diccionario se escribe `clave: valor`.');
|
|
1017
|
+
const value = this.grouped(() => this.parseExpression());
|
|
1018
|
+
if (key.kind === 'StringLiteral' || key.kind === 'NumberLiteral') {
|
|
1019
|
+
if (literalKeys.has(key.value)) {
|
|
1020
|
+
this.error('CLAVE_REPETIDA', `La clave ${describeKey(key.value)} está repetida.`, key.span);
|
|
1021
|
+
}
|
|
1022
|
+
literalKeys.add(key.value);
|
|
1023
|
+
if (value.kind === 'FunctionLiteral' && !value.name && key.kind === 'StringLiteral') value.name = key.value;
|
|
1024
|
+
}
|
|
1025
|
+
entries.push({ key, value, span: this.spanFrom(entryStart) });
|
|
1026
|
+
if (!this.match(TokenType.COMMA)) break;
|
|
1027
|
+
}
|
|
1028
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
1029
|
+
this.failSilentlyOnIllegal();
|
|
1030
|
+
this.error(
|
|
1031
|
+
'TOKEN_ESPERADO',
|
|
1032
|
+
`Se esperaba \`,\` o \`}\` en el diccionario que empieza en la línea ${open.span.start.line}, pero se encontró ${describeToken(this.current)}.`,
|
|
1033
|
+
this.current.span,
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
this.advance();
|
|
1037
|
+
return { kind: 'DictLiteral', id: this.id(), entries, span: this.spanFrom(open.span.start) };
|
|
1038
|
+
}
|
|
1039
|
+
|
|
624
1040
|
/** Expresiones separadas por comas hasta `closing`. Se permite una coma final. */
|
|
625
1041
|
private parseList_(closing: TokenType, context: string): ast.Expression[] {
|
|
626
1042
|
const items: ast.Expression[] = [];
|
|
627
1043
|
while (!this.check(closing)) {
|
|
628
|
-
items.push(this.parseExpression());
|
|
1044
|
+
items.push(this.grouped(() => this.parseExpression()));
|
|
629
1045
|
if (!this.match(TokenType.COMMA)) break;
|
|
630
1046
|
}
|
|
631
1047
|
this.expect(closing, context);
|
|
632
1048
|
return items;
|
|
633
1049
|
}
|
|
634
1050
|
|
|
635
|
-
private
|
|
1051
|
+
private parseFunctionDeclaration(): ast.FunctionDeclaration {
|
|
636
1052
|
const start = this.advance().span.start;
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
);
|
|
645
|
-
}
|
|
1053
|
+
const name = this.parseBindingName('después de `procedimiento`');
|
|
1054
|
+
const fn = this.parseFunction(start, name.name);
|
|
1055
|
+
return { kind: 'FunctionDeclaration', id: this.id(), name, function: fn, span: fn.span };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/** Parámetros y cuerpo; `procedimiento` (y el nombre, si lo hay) ya se leyeron. */
|
|
1059
|
+
private parseFunction(start: Position, name?: string): ast.FunctionLiteral {
|
|
646
1060
|
this.expect(
|
|
647
1061
|
TokenType.LPAREN,
|
|
648
1062
|
'después de `procedimiento`',
|
|
@@ -660,7 +1074,17 @@ class Parser {
|
|
|
660
1074
|
if (!this.match(TokenType.COMMA)) break;
|
|
661
1075
|
}
|
|
662
1076
|
this.expect(TokenType.RPAREN, 'para cerrar los parámetros');
|
|
663
|
-
|
|
664
|
-
|
|
1077
|
+
// `romper` y `continuar` no cruzan los límites de un procedimiento.
|
|
1078
|
+
const loopDepth = this.loopDepth;
|
|
1079
|
+
this.loopDepth = 0;
|
|
1080
|
+
let body: ast.Block;
|
|
1081
|
+
try {
|
|
1082
|
+
body = this.parseBlock('para el cuerpo del procedimiento');
|
|
1083
|
+
} finally {
|
|
1084
|
+
this.loopDepth = loopDepth;
|
|
1085
|
+
}
|
|
1086
|
+
return { kind: 'FunctionLiteral', id: this.id(), name, params, body, span: this.spanFrom(start) };
|
|
665
1087
|
}
|
|
666
1088
|
}
|
|
1089
|
+
|
|
1090
|
+
const describeKey = (key: string | number): string => (typeof key === 'string' ? JSON.stringify(key) : String(key));
|