codexivo 0.2.3 → 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.
@@ -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(private readonly tokens: Token[]) {}
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 !== TokenType.EOF) this.position += 1;
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
- if (this.errors.length < MAX_ERRORS) {
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) && !this.check(TokenType.EOF)) this.advance();
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.advance();
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.advance();
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.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
- }),
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
- return { kind: 'Block', id: this.id(), statements: [], span };
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.advance();
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
- return { kind: 'ExpressionStatement', id: this.id(), expression, span: this.spanFrom(start) };
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 { kind: 'Increment', id: this.id(), target: expression, operator: increment, span: this.spanFrom(start) };
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) || 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
- }),
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: 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) };
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
- /** `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 {
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 (precedence < (INFIX_PRECEDENCE[this.current.type] ?? Precedence.LOWEST)) {
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.advance();
827
- return { kind: 'NumberLiteral', id: this.id(), value: token.value as number, span: token.span };
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.advance();
830
- return { kind: 'StringLiteral', id: this.id(), value: token.value as string, span: token.span };
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.advance();
834
- return { kind: 'BooleanLiteral', id: this.id(), value: token.type === TokenType.TRUE, span: token.span };
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.advance();
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.advance();
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 { kind: 'Unary', id: this.id(), operator: '-', operand, span: this.spanFrom(token.span.start) };
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 { kind: 'Unary', id: this.id(), operator: 'no', operand, span: this.spanFrom(token.span.start) };
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.advance();
960
- const index: ast.StringLiteral = {
1155
+ const index = this.leaf<ast.StringLiteral>('StringLiteral', (_, id) => ({
961
1156
  kind: 'StringLiteral',
962
- id: this.id(),
1157
+ id,
963
1158
  value: property.lexeme,
964
1159
  span: property.span,
965
- };
966
- return { kind: 'Index', id: this.id(), object: left, index, dot: true, span: this.spanFrom(start) };
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