codexivo 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +22 -22
- package/dist/index.js.map +4 -4
- package/dist/worker.js +28 -28
- package/dist/worker.js.map +4 -4
- package/docs/arquitectura.md +46 -1
- package/docs/integracion.md +1 -0
- package/package.json +1 -1
- package/src/index.ts +2 -2
- package/src/sandbox-host.ts +15 -2
- package/src/syntax/parser.ts +295 -87
package/src/syntax/parser.ts
CHANGED
|
@@ -9,19 +9,72 @@ export interface ParseResult {
|
|
|
9
9
|
tokens: Token[];
|
|
10
10
|
/** Errores léxicos y sintácticos, en el orden en que aparecen en el código. */
|
|
11
11
|
errors: CodexivoError[];
|
|
12
|
+
/** Cómo se construyó el árbol, paso a paso. Solo con `{ trace: true }`. */
|
|
13
|
+
trace?: ParseEvent[];
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
export interface ParseOptions {
|
|
17
|
+
/** Registra en `trace` cada paso del parser. Sin esta opción el parser no hace ningún trabajo extra. */
|
|
18
|
+
trace?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Un operador en una decisión de precedencia. */
|
|
22
|
+
export interface PrecedenceOperator {
|
|
23
|
+
/** El texto del operador tal como está en el código (`+`, `y`, `es_igual_a`, `(` de una llamada…). */
|
|
24
|
+
operator: string;
|
|
25
|
+
/** La precedencia con la que se compara: más alta, agrupa antes. */
|
|
26
|
+
precedence: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Un paso del parser. Todos son objetos planos con su `span`: se pueden convertir a JSON o mandar desde un Web Worker.
|
|
31
|
+
*
|
|
32
|
+
* - `consume`: toma el token número `index` de `tokens`.
|
|
33
|
+
* - `enter` / `exit`: empieza o termina de construir un nodo. `exit.id` es el `id` del nodo en el AST. Un nodo que se
|
|
34
|
+
* construye a partir de otro que ya estaba hecho (el lado izquierdo de `1 + 2`, el procedimiento de una llamada, la
|
|
35
|
+
* variable de una asignación) lo indica en `enter.left`.
|
|
36
|
+
* - `precedence`: el parser Pratt decide de quién es `operand`. Si `incoming` tiene más precedencia que `building` (el
|
|
37
|
+
* operador que se está construyendo), `incoming` se lo lleva; si no, se queda con `building`. Sin `building`, no hay
|
|
38
|
+
* operador esperando ese valor.
|
|
39
|
+
* - `error`: un error de sintaxis (el mismo de `errors`).
|
|
40
|
+
* - `recover`: después de un error, el parser abandona los nodos que seguían abiertos (`abandoned`, del más interno al
|
|
41
|
+
* más externo; no tendrán `exit`), salta los tokens `skipped.from` a `skipped.to - 1` y sigue desde `skipped.to`.
|
|
42
|
+
*/
|
|
43
|
+
export type ParseEvent =
|
|
44
|
+
| { type: 'consume'; index: number; tokenType: TokenType; lexeme: string; span: Span }
|
|
45
|
+
| { type: 'enter'; kind: ast.Node['kind']; left?: number; span: Span }
|
|
46
|
+
| { type: 'exit'; kind: ast.Node['kind']; id: number; span: Span }
|
|
47
|
+
| {
|
|
48
|
+
type: 'precedence';
|
|
49
|
+
operand: number;
|
|
50
|
+
building?: PrecedenceOperator;
|
|
51
|
+
incoming: PrecedenceOperator;
|
|
52
|
+
decision: 'se_queda' | 'se_lo_lleva';
|
|
53
|
+
span: Span;
|
|
54
|
+
}
|
|
55
|
+
| { type: 'error'; code: string; message: string; span: Span }
|
|
56
|
+
| {
|
|
57
|
+
type: 'recover';
|
|
58
|
+
abandoned: ast.Node['kind'][];
|
|
59
|
+
skipped: { from: number; to: number };
|
|
60
|
+
span: Span;
|
|
61
|
+
};
|
|
62
|
+
|
|
14
63
|
/** Analiza el código y construye el AST. Nunca lanza: los problemas se reportan en `errors`. */
|
|
15
|
-
export const parse = (source: string): ParseResult => {
|
|
64
|
+
export const parse = (source: string, options: ParseOptions = {}): ParseResult => {
|
|
16
65
|
const { tokens, errors: lexicalErrors } = tokenize(source);
|
|
17
|
-
const parser = new Parser(tokens);
|
|
66
|
+
const parser = new Parser(tokens, options.trace ? [] : undefined);
|
|
18
67
|
const program = parser.parseProgram();
|
|
19
68
|
const errors = [...lexicalErrors, ...parser.errors].sort(
|
|
20
69
|
(a, b) => (a.span?.start.offset ?? 0) - (b.span?.start.offset ?? 0),
|
|
21
70
|
);
|
|
22
|
-
return { program, tokens, errors };
|
|
71
|
+
return parser.trace ? { program, tokens, errors, trace: parser.trace } : { program, tokens, errors };
|
|
23
72
|
};
|
|
24
73
|
|
|
74
|
+
/** `parse` con la traza: cómo se construyó el árbol, paso a paso. */
|
|
75
|
+
export const traceParse = (source: string): ParseResult & { trace: ParseEvent[] } =>
|
|
76
|
+
parse(source, { trace: true }) as ParseResult & { trace: ParseEvent[] };
|
|
77
|
+
|
|
25
78
|
enum Precedence {
|
|
26
79
|
LOWEST,
|
|
27
80
|
OR,
|
|
@@ -158,13 +211,20 @@ class Parser {
|
|
|
158
211
|
private grouping = 0;
|
|
159
212
|
/** Bucles abiertos alrededor de la sentencia actual, sin contar los de afuera de un procedimiento. */
|
|
160
213
|
private loopDepth = 0;
|
|
214
|
+
/** Con traza: los nodos con `enter` que todavía no tienen `exit`. */
|
|
215
|
+
private readonly open: ast.Node['kind'][] = [];
|
|
161
216
|
|
|
162
|
-
constructor(
|
|
217
|
+
constructor(
|
|
218
|
+
private readonly tokens: Token[],
|
|
219
|
+
/** Los pasos del parser, solo si se pidió la traza. */
|
|
220
|
+
readonly trace?: ParseEvent[],
|
|
221
|
+
) {}
|
|
163
222
|
|
|
164
223
|
parseProgram(): ast.Program {
|
|
165
224
|
const start = this.current.span.start;
|
|
225
|
+
this.begin('Program');
|
|
166
226
|
const statements = this.parseStatements(TokenType.EOF);
|
|
167
|
-
return { kind: 'Program', id: this.id(), statements, span: { start, end: this.current.span.end } };
|
|
227
|
+
return this.finish({ kind: 'Program', id: this.id(), statements, span: { start, end: this.current.span.end } });
|
|
168
228
|
}
|
|
169
229
|
|
|
170
230
|
// ── utilidades ─────────────────────────────────────────────────────────────
|
|
@@ -187,10 +247,76 @@ class Parser {
|
|
|
187
247
|
|
|
188
248
|
private advance(): Token {
|
|
189
249
|
const token = this.current;
|
|
190
|
-
if (token.type
|
|
250
|
+
if (token.type === TokenType.EOF) return token;
|
|
251
|
+
this.trace?.push({
|
|
252
|
+
type: 'consume',
|
|
253
|
+
index: this.position,
|
|
254
|
+
tokenType: token.type,
|
|
255
|
+
lexeme: token.lexeme,
|
|
256
|
+
span: token.span,
|
|
257
|
+
});
|
|
258
|
+
this.position += 1;
|
|
191
259
|
return token;
|
|
192
260
|
}
|
|
193
261
|
|
|
262
|
+
/** Salta un token al recuperarse de un error; en la traza va dentro de `recover`, no como `consume`. */
|
|
263
|
+
private skip(): void {
|
|
264
|
+
if (!this.check(TokenType.EOF)) this.position += 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── traza ──────────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
/** Empieza a construir un nodo. `left` es el nodo ya construido que el nuevo toma como primer hijo. */
|
|
270
|
+
private begin(kind: ast.Node['kind'], left?: ast.Node, span: Span = this.current.span): void {
|
|
271
|
+
if (!this.trace) return;
|
|
272
|
+
this.open.push(kind);
|
|
273
|
+
this.trace.push(left ? { type: 'enter', kind, left: left.id, span } : { type: 'enter', kind, span });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Termina de construir un nodo (su `begin` ya se registró). */
|
|
277
|
+
private finish<T extends ast.Node>(node: T): T {
|
|
278
|
+
if (!this.trace) return node;
|
|
279
|
+
this.open.pop();
|
|
280
|
+
this.trace.push({ type: 'exit', kind: node.kind, id: node.id, span: node.span });
|
|
281
|
+
return node;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Un nodo hecho de un solo token: `begin`, `consume` y `finish`. */
|
|
285
|
+
private leaf<T extends ast.Node>(kind: T['kind'], build: (token: Token, id: number) => T): T {
|
|
286
|
+
this.begin(kind);
|
|
287
|
+
const token = this.advance();
|
|
288
|
+
return this.finish(build(token, this.id()));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private tracePrecedence(
|
|
292
|
+
operand: ast.Expression,
|
|
293
|
+
precedence: Precedence,
|
|
294
|
+
owner: Token | undefined,
|
|
295
|
+
incoming: Precedence,
|
|
296
|
+
takes: boolean,
|
|
297
|
+
): void {
|
|
298
|
+
this.trace!.push({
|
|
299
|
+
type: 'precedence',
|
|
300
|
+
operand: operand.id,
|
|
301
|
+
...(owner ? { building: { operator: owner.lexeme, precedence } } : {}),
|
|
302
|
+
incoming: { operator: this.current.lexeme, precedence: incoming },
|
|
303
|
+
decision: takes ? 'se_lo_lleva' : 'se_queda',
|
|
304
|
+
span: this.current.span,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Después de un error: los nodos abiertos desde `openBefore` se abandonan y se saltaron los tokens desde `from`. */
|
|
309
|
+
private traceRecover(openBefore: number, from: number): void {
|
|
310
|
+
if (!this.trace) return;
|
|
311
|
+
const to = this.position;
|
|
312
|
+
const span =
|
|
313
|
+
to > from
|
|
314
|
+
? { start: this.tokens[from]!.span.start, end: this.tokens[to - 1]!.span.end }
|
|
315
|
+
: { start: this.current.span.start, end: this.current.span.start };
|
|
316
|
+
const abandoned = this.open.splice(openBefore).reverse();
|
|
317
|
+
this.trace.push({ type: 'recover', abandoned, skipped: { from, to }, span });
|
|
318
|
+
}
|
|
319
|
+
|
|
194
320
|
private check(type: TokenType): boolean {
|
|
195
321
|
return this.current.type === type;
|
|
196
322
|
}
|
|
@@ -206,12 +332,17 @@ class Parser {
|
|
|
206
332
|
}
|
|
207
333
|
|
|
208
334
|
private error(code: string, message: string, span: Span, hint?: string): never {
|
|
209
|
-
|
|
210
|
-
this.errors.push(new CodexivoError({ kind: 'sintaxis', code, message, hint, span }));
|
|
211
|
-
}
|
|
335
|
+
this.report(code, message, span, hint);
|
|
212
336
|
throw new ParseFailure();
|
|
213
337
|
}
|
|
214
338
|
|
|
339
|
+
/** Registra un error sin abandonar lo que se está analizando. */
|
|
340
|
+
private report(code: string, message: string, span: Span, hint?: string): void {
|
|
341
|
+
if (this.errors.length >= MAX_ERRORS) return;
|
|
342
|
+
this.errors.push(new CodexivoError({ kind: 'sintaxis', code, message, hint, span }));
|
|
343
|
+
this.trace?.push({ type: 'error', code, message, span });
|
|
344
|
+
}
|
|
345
|
+
|
|
215
346
|
/** Un token ILLEGAL ya tiene su error léxico; no lo reportamos dos veces. */
|
|
216
347
|
private failSilentlyOnIllegal(): void {
|
|
217
348
|
if (this.check(TokenType.ILLEGAL)) throw new ParseFailure();
|
|
@@ -251,13 +382,13 @@ class Parser {
|
|
|
251
382
|
|
|
252
383
|
/** Salta tokens hasta un punto seguro para seguir analizando después de un error. */
|
|
253
384
|
private synchronize(failedAt: Token): void {
|
|
254
|
-
if (this.position === this.tokens.indexOf(failedAt)
|
|
385
|
+
if (this.position === this.tokens.indexOf(failedAt)) this.skip();
|
|
255
386
|
while (!this.check(TokenType.EOF)) {
|
|
256
387
|
if (this.previous.type === TokenType.SEMICOLON) return;
|
|
257
388
|
if (this.check(TokenType.RBRACE)) return;
|
|
258
389
|
if (STATEMENT_KEYWORDS.has(this.current.type)) return;
|
|
259
390
|
if (this.current.span.start.line > failedAt.span.start.line) return;
|
|
260
|
-
this.
|
|
391
|
+
this.skip();
|
|
261
392
|
}
|
|
262
393
|
}
|
|
263
394
|
|
|
@@ -280,11 +411,14 @@ class Parser {
|
|
|
280
411
|
let recovering = false;
|
|
281
412
|
while (!this.check(terminator) && !this.check(TokenType.EOF)) {
|
|
282
413
|
const failedAt = this.current;
|
|
414
|
+
const openBefore = this.open.length;
|
|
283
415
|
try {
|
|
284
416
|
if (terminator === TokenType.EOF && this.check(TokenType.RBRACE)) {
|
|
285
417
|
// Una `}` sobrante después de un error suele ser parte de lo que ya se reportó.
|
|
286
418
|
if (recovering) {
|
|
287
|
-
this.
|
|
419
|
+
const from = this.position;
|
|
420
|
+
this.skip();
|
|
421
|
+
this.traceRecover(openBefore, from);
|
|
288
422
|
continue;
|
|
289
423
|
}
|
|
290
424
|
this.error('LLAVE_SOBRANTE', 'Sobra esta `}`: no hay ningún bloque abierto.', this.current.span);
|
|
@@ -295,7 +429,9 @@ class Parser {
|
|
|
295
429
|
if (!(error instanceof ParseFailure)) throw error;
|
|
296
430
|
this.depth = 0;
|
|
297
431
|
recovering = true;
|
|
432
|
+
const from = this.position;
|
|
298
433
|
this.synchronize(failedAt);
|
|
434
|
+
this.traceRecover(openBefore, from);
|
|
299
435
|
}
|
|
300
436
|
}
|
|
301
437
|
this.checkDuplicateProcedures(statements);
|
|
@@ -318,14 +454,11 @@ class Parser {
|
|
|
318
454
|
continue;
|
|
319
455
|
}
|
|
320
456
|
if (this.errors.length >= MAX_ERRORS) return;
|
|
321
|
-
this.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
hint: 'Un procedimiento con nombre existe desde el inicio de su bloque; cambia uno de los dos nombres.',
|
|
327
|
-
span: name.span,
|
|
328
|
-
}),
|
|
457
|
+
this.report(
|
|
458
|
+
'NOMBRE_REPETIDO',
|
|
459
|
+
`Ya hay un procedimiento llamado \`${name.name}\` en este bloque.`,
|
|
460
|
+
name.span,
|
|
461
|
+
'Un procedimiento con nombre existe desde el inicio de su bloque; cambia uno de los dos nombres.',
|
|
329
462
|
);
|
|
330
463
|
}
|
|
331
464
|
}
|
|
@@ -405,7 +538,8 @@ class Parser {
|
|
|
405
538
|
// `;;` o un `;` suelto no hacen nada; seguimos con lo que venga.
|
|
406
539
|
if (this.check(TokenType.EOF) || this.check(TokenType.RBRACE)) {
|
|
407
540
|
const span = this.previous.span;
|
|
408
|
-
|
|
541
|
+
this.begin('Block', undefined, span);
|
|
542
|
+
return this.finish({ kind: 'Block', id: this.id(), statements: [], span });
|
|
409
543
|
}
|
|
410
544
|
return this.parseStatement();
|
|
411
545
|
}
|
|
@@ -430,6 +564,7 @@ class Parser {
|
|
|
430
564
|
}
|
|
431
565
|
|
|
432
566
|
private parseVariableDeclaration(): ast.VariableDeclaration {
|
|
567
|
+
this.begin('VariableDeclaration');
|
|
433
568
|
const keyword = this.advance(); // variable / constante
|
|
434
569
|
const constant = keyword.type === TokenType.CONST;
|
|
435
570
|
const name = this.parseBindingName(`después de \`${keyword.lexeme}\``);
|
|
@@ -442,14 +577,14 @@ class Parser {
|
|
|
442
577
|
);
|
|
443
578
|
const value = this.parseExpression();
|
|
444
579
|
if (value.kind === 'FunctionLiteral' && !value.name) value.name = name.name;
|
|
445
|
-
return {
|
|
580
|
+
return this.finish({
|
|
446
581
|
kind: 'VariableDeclaration',
|
|
447
582
|
id: this.id(),
|
|
448
583
|
constant,
|
|
449
584
|
name,
|
|
450
585
|
value,
|
|
451
586
|
span: this.spanFrom(keyword.span.start),
|
|
452
|
-
};
|
|
587
|
+
});
|
|
453
588
|
}
|
|
454
589
|
|
|
455
590
|
private parseBindingName(context: string): ast.Identifier {
|
|
@@ -472,8 +607,7 @@ class Parser {
|
|
|
472
607
|
token.span,
|
|
473
608
|
);
|
|
474
609
|
}
|
|
475
|
-
this.
|
|
476
|
-
return { kind: 'Identifier', id: this.id(), name: token.lexeme, span: token.span };
|
|
610
|
+
return this.leaf('Identifier', (token, id) => ({ kind: 'Identifier', id, name: token.lexeme, span: token.span }));
|
|
477
611
|
}
|
|
478
612
|
|
|
479
613
|
/** Asignación (`x = …`, `lista[i] += …`), incremento (`i++`) o expresión. */
|
|
@@ -483,7 +617,8 @@ class Parser {
|
|
|
483
617
|
const operator = ASSIGNMENT_OPERATORS[this.current.type];
|
|
484
618
|
const increment = INCREMENT_OPERATORS[this.current.type];
|
|
485
619
|
if (!operator && !increment) {
|
|
486
|
-
|
|
620
|
+
this.begin('ExpressionStatement', expression, expression.span);
|
|
621
|
+
return this.finish({ kind: 'ExpressionStatement', id: this.id(), expression, span: this.spanFrom(start) });
|
|
487
622
|
}
|
|
488
623
|
if (expression.kind !== 'Identifier' && expression.kind !== 'Index') {
|
|
489
624
|
this.error(
|
|
@@ -494,25 +629,33 @@ class Parser {
|
|
|
494
629
|
this.current.span,
|
|
495
630
|
);
|
|
496
631
|
}
|
|
632
|
+
this.begin(increment ? 'Increment' : 'Assignment', expression);
|
|
497
633
|
this.advance();
|
|
498
634
|
if (increment) {
|
|
499
|
-
return
|
|
635
|
+
return this.finish({
|
|
636
|
+
kind: 'Increment',
|
|
637
|
+
id: this.id(),
|
|
638
|
+
target: expression,
|
|
639
|
+
operator: increment,
|
|
640
|
+
span: this.spanFrom(start),
|
|
641
|
+
});
|
|
500
642
|
}
|
|
501
643
|
const value = this.parseExpression();
|
|
502
644
|
if (value.kind === 'FunctionLiteral' && !value.name && expression.kind === 'Identifier') {
|
|
503
645
|
value.name = expression.name;
|
|
504
646
|
}
|
|
505
|
-
return {
|
|
647
|
+
return this.finish({
|
|
506
648
|
kind: 'Assignment',
|
|
507
649
|
id: this.id(),
|
|
508
650
|
target: expression,
|
|
509
651
|
operator: operator!,
|
|
510
652
|
value,
|
|
511
653
|
span: this.spanFrom(start),
|
|
512
|
-
};
|
|
654
|
+
});
|
|
513
655
|
}
|
|
514
656
|
|
|
515
657
|
private parseReturn(): ast.Return {
|
|
658
|
+
this.begin('Return');
|
|
516
659
|
const keyword = this.advance();
|
|
517
660
|
const endsHere =
|
|
518
661
|
this.check(TokenType.SEMICOLON) ||
|
|
@@ -520,10 +663,11 @@ class Parser {
|
|
|
520
663
|
this.check(TokenType.EOF) ||
|
|
521
664
|
this.current.span.start.line > keyword.span.end.line;
|
|
522
665
|
const value = endsHere ? undefined : this.parseExpression();
|
|
523
|
-
return { kind: 'Return', id: this.id(), value, span: this.spanFrom(keyword.span.start) };
|
|
666
|
+
return this.finish({ kind: 'Return', id: this.id(), value, span: this.spanFrom(keyword.span.start) });
|
|
524
667
|
}
|
|
525
668
|
|
|
526
669
|
private parseBlock(context: string): ast.Block {
|
|
670
|
+
this.begin('Block');
|
|
527
671
|
const open = this.expect(TokenType.LBRACE, context, 'Los bloques de Codexivo siempre van entre llaves `{ }`.');
|
|
528
672
|
const statements = this.parseStatements(TokenType.RBRACE);
|
|
529
673
|
if (!this.check(TokenType.RBRACE)) {
|
|
@@ -534,7 +678,7 @@ class Parser {
|
|
|
534
678
|
);
|
|
535
679
|
}
|
|
536
680
|
this.advance();
|
|
537
|
-
return { kind: 'Block', id: this.id(), statements, span: this.spanFrom(open.span.start) };
|
|
681
|
+
return this.finish({ kind: 'Block', id: this.id(), statements, span: this.spanFrom(open.span.start) });
|
|
538
682
|
}
|
|
539
683
|
|
|
540
684
|
/** Analiza `parse` dentro de paréntesis, corchetes o llaves, donde los saltos de línea no terminan nada. */
|
|
@@ -560,6 +704,7 @@ class Parser {
|
|
|
560
704
|
|
|
561
705
|
private parseIf(): ast.If {
|
|
562
706
|
const start = this.current.span.start;
|
|
707
|
+
this.begin('If');
|
|
563
708
|
const branches: ast.IfBranch[] = [];
|
|
564
709
|
let keyword = 'si';
|
|
565
710
|
do {
|
|
@@ -582,17 +727,19 @@ class Parser {
|
|
|
582
727
|
}
|
|
583
728
|
alternative = this.parseBlock('después de `si_no`');
|
|
584
729
|
}
|
|
585
|
-
return { kind: 'If', id: this.id(), branches, alternative, span: this.spanFrom(start) };
|
|
730
|
+
return this.finish({ kind: 'If', id: this.id(), branches, alternative, span: this.spanFrom(start) });
|
|
586
731
|
}
|
|
587
732
|
|
|
588
733
|
private parseWhile(): ast.While {
|
|
734
|
+
this.begin('While');
|
|
589
735
|
const start = this.advance().span.start;
|
|
590
736
|
const condition = this.parseCondition('mientras');
|
|
591
737
|
const body = this.parseLoopBody('después de la condición de `mientras`');
|
|
592
|
-
return { kind: 'While', id: this.id(), condition, body, span: this.spanFrom(start) };
|
|
738
|
+
return this.finish({ kind: 'While', id: this.id(), condition, body, span: this.spanFrom(start) });
|
|
593
739
|
}
|
|
594
740
|
|
|
595
741
|
private parseDoUntil(): ast.DoUntil {
|
|
742
|
+
this.begin('DoUntil');
|
|
596
743
|
const start = this.advance().span.start;
|
|
597
744
|
const body = this.parseLoopBody('después de `hacer`');
|
|
598
745
|
this.expect(
|
|
@@ -601,10 +748,11 @@ class Parser {
|
|
|
601
748
|
'La forma completa es `hacer { … } hasta_que (condición)`.',
|
|
602
749
|
);
|
|
603
750
|
const condition = this.parseCondition('hasta_que');
|
|
604
|
-
return { kind: 'DoUntil', id: this.id(), body, condition, span: this.spanFrom(start) };
|
|
751
|
+
return this.finish({ kind: 'DoUntil', id: this.id(), body, condition, span: this.spanFrom(start) });
|
|
605
752
|
}
|
|
606
753
|
|
|
607
754
|
private parseFor(): ast.For | ast.ForRange {
|
|
755
|
+
this.begin(this.peek().type === TokenType.LPAREN ? 'For' : 'ForRange');
|
|
608
756
|
const start = this.advance().span.start;
|
|
609
757
|
// `para i desde …` cuenta; `para (…; …; …)` es la forma clásica.
|
|
610
758
|
if (!this.check(TokenType.LPAREN)) return this.parseForRange(start);
|
|
@@ -632,7 +780,7 @@ class Parser {
|
|
|
632
780
|
});
|
|
633
781
|
|
|
634
782
|
const body = this.parseLoopBody('después de `para (…)`');
|
|
635
|
-
return { kind: 'For', id: this.id(), init, condition, update, body, span: this.spanFrom(start) };
|
|
783
|
+
return this.finish({ kind: 'For', id: this.id(), init, condition, update, body, span: this.spanFrom(start) });
|
|
636
784
|
}
|
|
637
785
|
|
|
638
786
|
private parseForRange(start: Position): ast.ForRange {
|
|
@@ -669,10 +817,11 @@ class Parser {
|
|
|
669
817
|
const to = value('hasta');
|
|
670
818
|
const step = this.match(TokenType.STEP) ? value('con_paso') : undefined;
|
|
671
819
|
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) };
|
|
820
|
+
return this.finish({ kind: 'ForRange', id: this.id(), variable, from, to, step, body, span: this.spanFrom(start) });
|
|
673
821
|
}
|
|
674
822
|
|
|
675
823
|
private parseRepeat(): ast.Repeat {
|
|
824
|
+
this.begin('Repeat');
|
|
676
825
|
const keyword = this.advance();
|
|
677
826
|
if (this.check(TokenType.TIMES) || this.check(TokenType.LBRACE)) {
|
|
678
827
|
this.error(
|
|
@@ -689,12 +838,14 @@ class Parser {
|
|
|
689
838
|
'La forma completa es `repetir 4 veces { … }`.',
|
|
690
839
|
);
|
|
691
840
|
const body = this.parseLoopBody('después de `repetir … veces`');
|
|
692
|
-
return { kind: 'Repeat', id: this.id(), count, body, span: this.spanFrom(keyword.span.start) };
|
|
841
|
+
return this.finish({ kind: 'Repeat', id: this.id(), count, body, span: this.spanFrom(keyword.span.start) });
|
|
693
842
|
}
|
|
694
843
|
|
|
695
844
|
private parseDomain(): ast.Domain {
|
|
845
|
+
this.begin('Domain');
|
|
696
846
|
const keyword = this.advance();
|
|
697
847
|
const name = this.parseBindingName('después de `dominio`');
|
|
848
|
+
this.begin('Block');
|
|
698
849
|
const open = this.expect(
|
|
699
850
|
TokenType.LBRACE,
|
|
700
851
|
`después de \`dominio ${name.name}\``,
|
|
@@ -718,24 +869,27 @@ class Parser {
|
|
|
718
869
|
}
|
|
719
870
|
this.advance();
|
|
720
871
|
for (const statement of statements) {
|
|
721
|
-
if (DOMAIN_MEMBERS.has(statement.kind)
|
|
722
|
-
this.
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
}),
|
|
872
|
+
if (DOMAIN_MEMBERS.has(statement.kind)) continue;
|
|
873
|
+
this.report(
|
|
874
|
+
'DOMINIO_SOLO_DECLARACIONES',
|
|
875
|
+
printStart(statement)
|
|
876
|
+
? `Un dominio solo agrupa declaraciones, y \`${printStart(statement)}\` no es una.`
|
|
877
|
+
: 'Un dominio solo agrupa declaraciones, y esta sentencia no es una.',
|
|
878
|
+
statement.span,
|
|
879
|
+
'Dentro de `dominio` van `variable`, `constante`, `procedimiento` u otro `dominio`. Lo demás va en un procedimiento o fuera del dominio.',
|
|
732
880
|
);
|
|
733
881
|
}
|
|
734
|
-
const body
|
|
735
|
-
|
|
882
|
+
const body = this.finish<ast.Block>({
|
|
883
|
+
kind: 'Block',
|
|
884
|
+
id: this.id(),
|
|
885
|
+
statements,
|
|
886
|
+
span: this.spanFrom(open.span.start),
|
|
887
|
+
});
|
|
888
|
+
return this.finish({ kind: 'Domain', id: this.id(), name, body, span: this.spanFrom(keyword.span.start) });
|
|
736
889
|
}
|
|
737
890
|
|
|
738
891
|
private parseForEach(): ast.ForEach {
|
|
892
|
+
this.begin('ForEach');
|
|
739
893
|
const keyword = this.advance();
|
|
740
894
|
const form = 'La forma completa es `para_cada fruta en frutas { … }`, sin paréntesis.';
|
|
741
895
|
if (this.check(TokenType.LPAREN)) {
|
|
@@ -753,7 +907,7 @@ class Parser {
|
|
|
753
907
|
this.expect(TokenType.IN, `después de \`para_cada ${names}\``, form);
|
|
754
908
|
const iterable = this.parseExpression();
|
|
755
909
|
const body = this.parseLoopBody('después de `para_cada … en …`');
|
|
756
|
-
return {
|
|
910
|
+
return this.finish({
|
|
757
911
|
kind: 'ForEach',
|
|
758
912
|
id: this.id(),
|
|
759
913
|
variable,
|
|
@@ -761,7 +915,7 @@ class Parser {
|
|
|
761
915
|
iterable,
|
|
762
916
|
body,
|
|
763
917
|
span: this.spanFrom(keyword.span.start),
|
|
764
|
-
};
|
|
918
|
+
});
|
|
765
919
|
}
|
|
766
920
|
|
|
767
921
|
/** El cuerpo de un bucle: ahí sí se puede usar `romper` y `continuar`. */
|
|
@@ -775,8 +929,9 @@ class Parser {
|
|
|
775
929
|
}
|
|
776
930
|
|
|
777
931
|
private parseLoopJump(): ast.Break | ast.Continue {
|
|
932
|
+
const kind = this.check(TokenType.BREAK) ? 'Break' : 'Continue';
|
|
933
|
+
this.begin(kind);
|
|
778
934
|
const token = this.advance();
|
|
779
|
-
const kind = token.type === TokenType.BREAK ? 'Break' : 'Continue';
|
|
780
935
|
if (this.loopDepth === 0) {
|
|
781
936
|
this.error(
|
|
782
937
|
kind === 'Break' ? 'ROMPER_FUERA_DE_BUCLE' : 'CONTINUAR_FUERA_DE_BUCLE',
|
|
@@ -787,18 +942,31 @@ class Parser {
|
|
|
787
942
|
: 'Dentro de un procedimiento, `continuar` no afecta al bucle desde donde se llamó.',
|
|
788
943
|
);
|
|
789
944
|
}
|
|
790
|
-
return { kind, id: this.id(), span: token.span };
|
|
945
|
+
return this.finish({ kind, id: this.id(), span: token.span });
|
|
791
946
|
}
|
|
792
947
|
|
|
793
948
|
// ── expresiones ────────────────────────────────────────────────────────────
|
|
794
949
|
|
|
795
|
-
/**
|
|
796
|
-
|
|
950
|
+
/**
|
|
951
|
+
* `statement` indica que la expresión es una sentencia completa, donde `i++` sí está permitido. `owner` es el operador
|
|
952
|
+
* que espera esta expresión como su lado derecho (o su operando), con precedencia `precedence`; solo se usa en la traza.
|
|
953
|
+
*/
|
|
954
|
+
private parseExpression(
|
|
955
|
+
precedence: Precedence = Precedence.LOWEST,
|
|
956
|
+
statement = false,
|
|
957
|
+
owner?: Token,
|
|
958
|
+
): ast.Expression {
|
|
797
959
|
this.enter();
|
|
798
960
|
let left = this.parsePrefix();
|
|
799
|
-
while (
|
|
961
|
+
while (true) {
|
|
962
|
+
const incoming = INFIX_PRECEDENCE[this.current.type];
|
|
963
|
+
if (incoming === undefined) break;
|
|
800
964
|
// Sin `;`, una línea que empieza con `-`, `(` o `[` es otra sentencia, no la continuación de la anterior.
|
|
801
965
|
if (this.grouping === 0 && this.current.span.start.line > this.previous.span.end.line) break;
|
|
966
|
+
// La decisión del parser Pratt: si el operador que llega agrupa más fuerte, se lleva a `left`.
|
|
967
|
+
const takes = precedence < incoming;
|
|
968
|
+
if (this.trace) this.tracePrecedence(left, precedence, owner, incoming, takes);
|
|
969
|
+
if (!takes) break;
|
|
802
970
|
left = this.parseInfix(left);
|
|
803
971
|
}
|
|
804
972
|
if (!statement && INCREMENT_OPERATORS[this.current.type]) this.incrementInExpression(left);
|
|
@@ -823,21 +991,31 @@ class Parser {
|
|
|
823
991
|
const token = this.current;
|
|
824
992
|
switch (token.type) {
|
|
825
993
|
case TokenType.NUMBER:
|
|
826
|
-
this.
|
|
827
|
-
|
|
994
|
+
return this.leaf('NumberLiteral', (_, id) => ({
|
|
995
|
+
kind: 'NumberLiteral',
|
|
996
|
+
id,
|
|
997
|
+
value: token.value as number,
|
|
998
|
+
span: token.span,
|
|
999
|
+
}));
|
|
828
1000
|
case TokenType.STRING:
|
|
829
|
-
this.
|
|
830
|
-
|
|
1001
|
+
return this.leaf('StringLiteral', (_, id) => ({
|
|
1002
|
+
kind: 'StringLiteral',
|
|
1003
|
+
id,
|
|
1004
|
+
value: token.value as string,
|
|
1005
|
+
span: token.span,
|
|
1006
|
+
}));
|
|
831
1007
|
case TokenType.TRUE:
|
|
832
1008
|
case TokenType.FALSE:
|
|
833
|
-
this.
|
|
834
|
-
|
|
1009
|
+
return this.leaf('BooleanLiteral', (_, id) => ({
|
|
1010
|
+
kind: 'BooleanLiteral',
|
|
1011
|
+
id,
|
|
1012
|
+
value: token.type === TokenType.TRUE,
|
|
1013
|
+
span: token.span,
|
|
1014
|
+
}));
|
|
835
1015
|
case TokenType.NULL:
|
|
836
|
-
this.
|
|
837
|
-
return { kind: 'NullLiteral', id: this.id(), span: token.span };
|
|
1016
|
+
return this.leaf('NullLiteral', (_, id) => ({ kind: 'NullLiteral', id, span: token.span }));
|
|
838
1017
|
case TokenType.IDENT:
|
|
839
|
-
this.
|
|
840
|
-
return { kind: 'Identifier', id: this.id(), name: token.lexeme, span: token.span };
|
|
1018
|
+
return this.leaf('Identifier', (_, id) => ({ kind: 'Identifier', id, name: token.lexeme, span: token.span }));
|
|
841
1019
|
case TokenType.LPAREN: {
|
|
842
1020
|
this.advance();
|
|
843
1021
|
const expression = this.grouped(() => this.parseExpression());
|
|
@@ -849,15 +1027,29 @@ class Parser {
|
|
|
849
1027
|
case TokenType.LBRACE:
|
|
850
1028
|
return this.parseDict();
|
|
851
1029
|
case TokenType.MINUS: {
|
|
1030
|
+
this.begin('Unary');
|
|
852
1031
|
this.advance();
|
|
853
|
-
const operand = this.parseExpression(Precedence.UNARY);
|
|
854
|
-
return
|
|
1032
|
+
const operand = this.parseExpression(Precedence.UNARY, false, token);
|
|
1033
|
+
return this.finish({
|
|
1034
|
+
kind: 'Unary',
|
|
1035
|
+
id: this.id(),
|
|
1036
|
+
operator: '-',
|
|
1037
|
+
operand,
|
|
1038
|
+
span: this.spanFrom(token.span.start),
|
|
1039
|
+
});
|
|
855
1040
|
}
|
|
856
1041
|
case TokenType.NOT:
|
|
857
1042
|
case TokenType.BANG: {
|
|
1043
|
+
this.begin('Unary');
|
|
858
1044
|
this.advance();
|
|
859
|
-
const operand = this.parseExpression(Precedence.NOT);
|
|
860
|
-
return
|
|
1045
|
+
const operand = this.parseExpression(Precedence.NOT, false, token);
|
|
1046
|
+
return this.finish({
|
|
1047
|
+
kind: 'Unary',
|
|
1048
|
+
id: this.id(),
|
|
1049
|
+
operator: 'no',
|
|
1050
|
+
operand,
|
|
1051
|
+
span: this.spanFrom(token.span.start),
|
|
1052
|
+
});
|
|
861
1053
|
}
|
|
862
1054
|
case TokenType.FUNCTION:
|
|
863
1055
|
if (this.peek().type === TokenType.IDENT) {
|
|
@@ -868,6 +1060,7 @@ class Parser {
|
|
|
868
1060
|
'Como valor, escríbelo sin nombre: `variable doble = procedimiento(x) { … }`.',
|
|
869
1061
|
);
|
|
870
1062
|
}
|
|
1063
|
+
this.begin('FunctionLiteral');
|
|
871
1064
|
return this.parseFunction(this.advance().span.start);
|
|
872
1065
|
case TokenType.INCREMENT:
|
|
873
1066
|
case TokenType.DECREMENT:
|
|
@@ -924,11 +1117,13 @@ class Parser {
|
|
|
924
1117
|
const start = left.span.start;
|
|
925
1118
|
switch (token.type) {
|
|
926
1119
|
case TokenType.LPAREN: {
|
|
1120
|
+
this.begin('Call', left);
|
|
927
1121
|
this.advance();
|
|
928
1122
|
const args = this.parseList_(TokenType.RPAREN, 'para cerrar los argumentos');
|
|
929
|
-
return { kind: 'Call', id: this.id(), callee: left, args, span: this.spanFrom(start) };
|
|
1123
|
+
return this.finish({ kind: 'Call', id: this.id(), callee: left, args, span: this.spanFrom(start) });
|
|
930
1124
|
}
|
|
931
1125
|
case TokenType.LBRACKET: {
|
|
1126
|
+
this.begin('Index', left);
|
|
932
1127
|
this.advance();
|
|
933
1128
|
if (this.check(TokenType.RBRACKET)) {
|
|
934
1129
|
this.error(
|
|
@@ -940,9 +1135,10 @@ class Parser {
|
|
|
940
1135
|
}
|
|
941
1136
|
const index = this.grouped(() => this.parseExpression());
|
|
942
1137
|
this.expect(TokenType.RBRACKET, 'para cerrar el índice');
|
|
943
|
-
return { kind: 'Index', id: this.id(), object: left, index, span: this.spanFrom(start) };
|
|
1138
|
+
return this.finish({ kind: 'Index', id: this.id(), object: left, index, span: this.spanFrom(start) });
|
|
944
1139
|
}
|
|
945
1140
|
case TokenType.DOT: {
|
|
1141
|
+
this.begin('Index', left);
|
|
946
1142
|
this.advance();
|
|
947
1143
|
const property = this.current;
|
|
948
1144
|
if (property.type !== TokenType.IDENT) {
|
|
@@ -956,28 +1152,36 @@ class Parser {
|
|
|
956
1152
|
'Con `.` solo se usan claves que son nombres válidos; para las demás escribe `d["clave"]`.',
|
|
957
1153
|
);
|
|
958
1154
|
}
|
|
959
|
-
this.
|
|
960
|
-
const index: ast.StringLiteral = {
|
|
1155
|
+
const index = this.leaf<ast.StringLiteral>('StringLiteral', (_, id) => ({
|
|
961
1156
|
kind: 'StringLiteral',
|
|
962
|
-
id
|
|
1157
|
+
id,
|
|
963
1158
|
value: property.lexeme,
|
|
964
1159
|
span: property.span,
|
|
965
|
-
};
|
|
966
|
-
return
|
|
1160
|
+
}));
|
|
1161
|
+
return this.finish({
|
|
1162
|
+
kind: 'Index',
|
|
1163
|
+
id: this.id(),
|
|
1164
|
+
object: left,
|
|
1165
|
+
index,
|
|
1166
|
+
dot: true,
|
|
1167
|
+
span: this.spanFrom(start),
|
|
1168
|
+
});
|
|
967
1169
|
}
|
|
968
1170
|
case TokenType.AND:
|
|
969
1171
|
case TokenType.OR: {
|
|
1172
|
+
this.begin('Logical', left);
|
|
970
1173
|
this.advance();
|
|
971
1174
|
const precedence = INFIX_PRECEDENCE[token.type]!;
|
|
972
|
-
const right = this.parseExpression(precedence);
|
|
1175
|
+
const right = this.parseExpression(precedence, false, token);
|
|
973
1176
|
const operator = token.type === TokenType.AND ? 'y' : 'o';
|
|
974
|
-
return { kind: 'Logical', id: this.id(), operator, left, right, span: this.spanFrom(start) };
|
|
1177
|
+
return this.finish({ kind: 'Logical', id: this.id(), operator, left, right, span: this.spanFrom(start) });
|
|
975
1178
|
}
|
|
976
1179
|
default: {
|
|
1180
|
+
this.begin('Binary', left);
|
|
977
1181
|
this.advance();
|
|
978
1182
|
const precedence = INFIX_PRECEDENCE[token.type]!;
|
|
979
1183
|
// `^` es asociativo a la derecha: `2 ^ 3 ^ 2` es `2 ^ (3 ^ 2)`.
|
|
980
|
-
const right = this.parseExpression(precedence === Precedence.POWER ? precedence - 1 : precedence);
|
|
1184
|
+
const right = this.parseExpression(precedence === Precedence.POWER ? precedence - 1 : precedence, false, token);
|
|
981
1185
|
const operator = BINARY_OPERATORS[token.type]!;
|
|
982
1186
|
if (precedence === Precedence.COMPARE && INFIX_PRECEDENCE[this.current.type] === Precedence.COMPARE) {
|
|
983
1187
|
this.error(
|
|
@@ -987,18 +1191,20 @@ class Parser {
|
|
|
987
1191
|
'Une las dos comparaciones con `y`, por ejemplo: `1 < x y x < 5`.',
|
|
988
1192
|
);
|
|
989
1193
|
}
|
|
990
|
-
return { kind: 'Binary', id: this.id(), operator, left, right, span: this.spanFrom(start) };
|
|
1194
|
+
return this.finish({ kind: 'Binary', id: this.id(), operator, left, right, span: this.spanFrom(start) });
|
|
991
1195
|
}
|
|
992
1196
|
}
|
|
993
1197
|
}
|
|
994
1198
|
|
|
995
1199
|
private parseList(): ast.ListLiteral {
|
|
1200
|
+
this.begin('ListLiteral');
|
|
996
1201
|
const start = this.advance().span.start;
|
|
997
1202
|
const elements = this.parseList_(TokenType.RBRACKET, 'para cerrar la lista');
|
|
998
|
-
return { kind: 'ListLiteral', id: this.id(), elements, span: this.spanFrom(start) };
|
|
1203
|
+
return this.finish({ kind: 'ListLiteral', id: this.id(), elements, span: this.spanFrom(start) });
|
|
999
1204
|
}
|
|
1000
1205
|
|
|
1001
1206
|
private parseDict(): ast.DictLiteral {
|
|
1207
|
+
this.begin('DictLiteral');
|
|
1002
1208
|
const open = this.advance();
|
|
1003
1209
|
const entries: ast.DictEntry[] = [];
|
|
1004
1210
|
const literalKeys = new Set<string | number>();
|
|
@@ -1034,7 +1240,7 @@ class Parser {
|
|
|
1034
1240
|
);
|
|
1035
1241
|
}
|
|
1036
1242
|
this.advance();
|
|
1037
|
-
return { kind: 'DictLiteral', id: this.id(), entries, span: this.spanFrom(open.span.start) };
|
|
1243
|
+
return this.finish({ kind: 'DictLiteral', id: this.id(), entries, span: this.spanFrom(open.span.start) });
|
|
1038
1244
|
}
|
|
1039
1245
|
|
|
1040
1246
|
/** Expresiones separadas por comas hasta `closing`. Se permite una coma final. */
|
|
@@ -1049,13 +1255,15 @@ class Parser {
|
|
|
1049
1255
|
}
|
|
1050
1256
|
|
|
1051
1257
|
private parseFunctionDeclaration(): ast.FunctionDeclaration {
|
|
1258
|
+
this.begin('FunctionDeclaration');
|
|
1052
1259
|
const start = this.advance().span.start;
|
|
1053
1260
|
const name = this.parseBindingName('después de `procedimiento`');
|
|
1261
|
+
this.begin('FunctionLiteral');
|
|
1054
1262
|
const fn = this.parseFunction(start, name.name);
|
|
1055
|
-
return { kind: 'FunctionDeclaration', id: this.id(), name, function: fn, span: fn.span };
|
|
1263
|
+
return this.finish({ kind: 'FunctionDeclaration', id: this.id(), name, function: fn, span: fn.span });
|
|
1056
1264
|
}
|
|
1057
1265
|
|
|
1058
|
-
/** Parámetros y cuerpo; `procedimiento` (y el nombre, si lo hay) ya se leyeron. */
|
|
1266
|
+
/** Parámetros y cuerpo; `procedimiento` (y el nombre, si lo hay) ya se leyeron y el nodo ya empezó (`begin`). */
|
|
1059
1267
|
private parseFunction(start: Position, name?: string): ast.FunctionLiteral {
|
|
1060
1268
|
this.expect(
|
|
1061
1269
|
TokenType.LPAREN,
|
|
@@ -1083,7 +1291,7 @@ class Parser {
|
|
|
1083
1291
|
} finally {
|
|
1084
1292
|
this.loopDepth = loopDepth;
|
|
1085
1293
|
}
|
|
1086
|
-
return { kind: 'FunctionLiteral', id: this.id(), name, params, body, span: this.spanFrom(start) };
|
|
1294
|
+
return this.finish({ kind: 'FunctionLiteral', id: this.id(), name, params, body, span: this.spanFrom(start) });
|
|
1087
1295
|
}
|
|
1088
1296
|
}
|
|
1089
1297
|
|