codexivo 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 `==`; un solo `=` sirve para asignar.';
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
- if (ASSIGNMENT_OPERATORS[next.type]) {
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.parseExpressionStatement());
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 start = this.advance().span.start; // variable
310
- const name = this.parseBindingName('después de `variable`');
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 \`variable ${name.name}\``,
314
- `Toda variable empieza con un valor: \`variable ${name.name} = 0\`.`,
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 { kind: 'VariableDeclaration', id: this.id(), name, value, span: this.spanFrom(start) };
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
- private parseAssignment(): ast.Assignment {
344
- const targetToken = this.advance();
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
- if (ASSIGNMENT_OPERATORS[this.current.type]) {
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
- 'Solo se le puede asignar un valor a una variable.',
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
- return { kind: 'ExpressionStatement', id: this.id(), expression, span: this.spanFrom(start) };
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.parseBlock('después de la condición de `mientras`');
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.parseBlock('después de `hacer`');
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
- let init: ast.For['init'];
464
- if (!this.check(TokenType.SEMICOLON)) {
465
- if (this.check(TokenType.LET)) init = this.parseVariableDeclaration();
466
- else if (this.check(TokenType.IDENT) && ASSIGNMENT_OPERATORS[this.peek().type]) init = this.parseAssignment();
467
- else init = this.parseExpressionStatement();
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.expect(TokenType.SEMICOLON, 'después del inicio del `para`');
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
- const condition = this.check(TokenType.SEMICOLON) ? undefined : this.parseExpression();
472
- this.expect(TokenType.SEMICOLON, 'después de la condición del `para`');
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
- let update: ast.For['update'];
475
- if (!this.check(TokenType.RPAREN)) {
476
- if (this.check(TokenType.IDENT) && ASSIGNMENT_OPERATORS[this.peek().type]) update = this.parseAssignment();
477
- else update = this.parseExpressionStatement();
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.expect(TokenType.RPAREN, 'para cerrar el `para`');
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
- const body = this.parseBlock('después de `para (…)`');
482
- return { kind: 'For', id: this.id(), init, condition, update, body, span: this.spanFrom(start) };
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
- private parseExpression(precedence: Precedence = Precedence.LOWEST): ast.Expression {
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
- return this.parseFunction();
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
- const right = this.parseExpression(precedence);
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 parseFunction(): ast.FunctionLiteral {
1051
+ private parseFunctionDeclaration(): ast.FunctionDeclaration {
636
1052
  const start = this.advance().span.start;
637
- if (this.check(TokenType.IDENT)) {
638
- const name = this.current.lexeme;
639
- this.error(
640
- 'PROCEDIMIENTO_CON_NOMBRE',
641
- 'Los procedimientos con nombre todavía no están disponibles.',
642
- this.current.span,
643
- `Por ahora escribe \`variable ${name} = procedimiento(…) { … };\`.`,
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
- const body = this.parseBlock('para el cuerpo del procedimiento');
664
- return { kind: 'FunctionLiteral', id: this.id(), params, body, span: this.spanFrom(start) };
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));