codexivo 0.0.0-stage → 0.2.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.
@@ -0,0 +1,666 @@
1
+ import { CodexivoError } from '../errors';
2
+ import type * as ast from './ast';
3
+ import { tokenize } from './lexer';
4
+ import { describeToken, describeTokenType, TokenType } from './token';
5
+ import type { Position, Span, Token } from './token';
6
+
7
+ export interface ParseResult {
8
+ program: ast.Program;
9
+ tokens: Token[];
10
+ /** Errores léxicos y sintácticos, en el orden en que aparecen en el código. */
11
+ errors: CodexivoError[];
12
+ }
13
+
14
+ /** Analiza el código y construye el AST. Nunca lanza: los problemas se reportan en `errors`. */
15
+ export const parse = (source: string): ParseResult => {
16
+ const { tokens, errors: lexicalErrors } = tokenize(source);
17
+ const parser = new Parser(tokens);
18
+ const program = parser.parseProgram();
19
+ const errors = [...lexicalErrors, ...parser.errors].sort(
20
+ (a, b) => (a.span?.start.offset ?? 0) - (b.span?.start.offset ?? 0),
21
+ );
22
+ return { program, tokens, errors };
23
+ };
24
+
25
+ enum Precedence {
26
+ LOWEST,
27
+ OR,
28
+ AND,
29
+ NOT,
30
+ COMPARE,
31
+ SUM,
32
+ PRODUCT,
33
+ UNARY,
34
+ CALL,
35
+ }
36
+
37
+ const INFIX_PRECEDENCE: Partial<Record<TokenType, Precedence>> = {
38
+ [TokenType.OR]: Precedence.OR,
39
+ [TokenType.AND]: Precedence.AND,
40
+ [TokenType.EQ]: Precedence.COMPARE,
41
+ [TokenType.NEQ]: Precedence.COMPARE,
42
+ [TokenType.LT]: Precedence.COMPARE,
43
+ [TokenType.GT]: Precedence.COMPARE,
44
+ [TokenType.LTE]: Precedence.COMPARE,
45
+ [TokenType.GTE]: Precedence.COMPARE,
46
+ [TokenType.PLUS]: Precedence.SUM,
47
+ [TokenType.MINUS]: Precedence.SUM,
48
+ [TokenType.STAR]: Precedence.PRODUCT,
49
+ [TokenType.SLASH]: Precedence.PRODUCT,
50
+ [TokenType.LPAREN]: Precedence.CALL,
51
+ [TokenType.LBRACKET]: Precedence.CALL,
52
+ };
53
+
54
+ const BINARY_OPERATORS: Partial<Record<TokenType, ast.BinaryOperator>> = {
55
+ [TokenType.PLUS]: '+',
56
+ [TokenType.MINUS]: '-',
57
+ [TokenType.STAR]: '*',
58
+ [TokenType.SLASH]: '/',
59
+ [TokenType.EQ]: '==',
60
+ [TokenType.NEQ]: '!=',
61
+ [TokenType.LT]: '<',
62
+ [TokenType.GT]: '>',
63
+ [TokenType.LTE]: '<=',
64
+ [TokenType.GTE]: '>=',
65
+ };
66
+
67
+ const ASSIGNMENT_OPERATORS: Partial<Record<TokenType, ast.AssignmentOperator>> = {
68
+ [TokenType.ASSIGN]: '=',
69
+ [TokenType.PLUS_ASSIGN]: '+=',
70
+ [TokenType.MINUS_ASSIGN]: '-=',
71
+ [TokenType.STAR_ASSIGN]: '*=',
72
+ [TokenType.SLASH_ASSIGN]: '/=',
73
+ };
74
+
75
+ const STATEMENT_KEYWORDS = new Set([
76
+ TokenType.LET,
77
+ TokenType.RETURN,
78
+ TokenType.IF,
79
+ TokenType.WHILE,
80
+ TokenType.DO,
81
+ TokenType.FOR,
82
+ ]);
83
+
84
+ const KEYWORD_LIKE_TYPOS: Readonly<Record<string, string>> = {
85
+ sino: 'En Codexivo se escribe `si_no`, con guion bajo.',
86
+ pero: 'En Codexivo se escribe `pero_si`, con guion bajo.',
87
+ hasta: 'En Codexivo se escribe `hasta_que`, con guion bajo.',
88
+ };
89
+
90
+ const MAX_ERRORS = 50;
91
+ const MAX_DEPTH = 400;
92
+
93
+ /** Señal interna para abandonar la sentencia actual. El error, si lo hay, ya se registró. */
94
+ class ParseFailure {}
95
+
96
+ class Parser {
97
+ readonly errors: CodexivoError[] = [];
98
+ private position = 0;
99
+ private nextId = 1;
100
+ private depth = 0;
101
+
102
+ constructor(private readonly tokens: Token[]) {}
103
+
104
+ parseProgram(): ast.Program {
105
+ const start = this.current.span.start;
106
+ const statements = this.parseStatements(TokenType.EOF);
107
+ return { kind: 'Program', id: this.id(), statements, span: { start, end: this.current.span.end } };
108
+ }
109
+
110
+ // ── utilidades ─────────────────────────────────────────────────────────────
111
+
112
+ private get current(): Token {
113
+ return this.tokens[this.position] ?? this.tokens[this.tokens.length - 1]!;
114
+ }
115
+
116
+ private get previous(): Token {
117
+ return this.tokens[this.position - 1] ?? this.current;
118
+ }
119
+
120
+ private peek(ahead = 1): Token {
121
+ return this.tokens[this.position + ahead] ?? this.tokens[this.tokens.length - 1]!;
122
+ }
123
+
124
+ private id(): number {
125
+ return this.nextId++;
126
+ }
127
+
128
+ private advance(): Token {
129
+ const token = this.current;
130
+ if (token.type !== TokenType.EOF) this.position += 1;
131
+ return token;
132
+ }
133
+
134
+ private check(type: TokenType): boolean {
135
+ return this.current.type === type;
136
+ }
137
+
138
+ private match(type: TokenType): boolean {
139
+ if (!this.check(type)) return false;
140
+ this.advance();
141
+ return true;
142
+ }
143
+
144
+ private spanFrom(start: Position): Span {
145
+ return { start, end: this.previous.span.end };
146
+ }
147
+
148
+ private error(code: string, message: string, span: Span, hint?: string): never {
149
+ if (this.errors.length < MAX_ERRORS) {
150
+ this.errors.push(new CodexivoError({ kind: 'sintaxis', code, message, hint, span }));
151
+ }
152
+ throw new ParseFailure();
153
+ }
154
+
155
+ /** Un token ILLEGAL ya tiene su error léxico; no lo reportamos dos veces. */
156
+ private failSilentlyOnIllegal(): void {
157
+ if (this.check(TokenType.ILLEGAL)) throw new ParseFailure();
158
+ }
159
+
160
+ private expect(type: TokenType, context: string, hint?: string): Token {
161
+ if (this.check(type)) return this.advance();
162
+ this.failSilentlyOnIllegal();
163
+ const found = this.current;
164
+ let finalHint = hint;
165
+ if (type === TokenType.RPAREN && found.type === TokenType.ASSIGN) {
166
+ finalHint = 'Para comparar se usa `==`; un solo `=` sirve para asignar.';
167
+ }
168
+ this.error(
169
+ 'TOKEN_ESPERADO',
170
+ `Se esperaba ${describeTokenType(type)} ${context}, pero se encontró ${describeToken(found)}.`,
171
+ found.span,
172
+ finalHint,
173
+ );
174
+ }
175
+
176
+ private enter(): void {
177
+ this.depth += 1;
178
+ if (this.depth > MAX_DEPTH) {
179
+ this.error(
180
+ 'ANIDAMIENTO_EXCESIVO',
181
+ 'El programa tiene demasiados niveles anidados.',
182
+ this.current.span,
183
+ 'Divide la expresión en variables intermedias.',
184
+ );
185
+ }
186
+ }
187
+
188
+ private leave(): void {
189
+ this.depth -= 1;
190
+ }
191
+
192
+ /** Salta tokens hasta un punto seguro para seguir analizando después de un error. */
193
+ private synchronize(failedAt: Token): void {
194
+ if (this.position === this.tokens.indexOf(failedAt) && !this.check(TokenType.EOF)) this.advance();
195
+ while (!this.check(TokenType.EOF)) {
196
+ if (this.previous.type === TokenType.SEMICOLON) return;
197
+ if (this.check(TokenType.RBRACE)) return;
198
+ if (STATEMENT_KEYWORDS.has(this.current.type)) return;
199
+ if (this.current.span.start.line > failedAt.span.start.line) return;
200
+ this.advance();
201
+ }
202
+ }
203
+
204
+ // ── sentencias ─────────────────────────────────────────────────────────────
205
+
206
+ private parseStatements(terminator: TokenType.EOF | TokenType.RBRACE): ast.Statement[] {
207
+ const statements: ast.Statement[] = [];
208
+ let recovering = false;
209
+ while (!this.check(terminator) && !this.check(TokenType.EOF)) {
210
+ const failedAt = this.current;
211
+ try {
212
+ if (terminator === TokenType.EOF && this.check(TokenType.RBRACE)) {
213
+ // Una `}` sobrante después de un error suele ser parte de lo que ya se reportó.
214
+ if (recovering) {
215
+ this.advance();
216
+ continue;
217
+ }
218
+ this.error('LLAVE_SOBRANTE', 'Sobra esta `}`: no hay ningún bloque abierto.', this.current.span);
219
+ }
220
+ statements.push(this.parseStatement());
221
+ recovering = false;
222
+ } catch (error) {
223
+ if (!(error instanceof ParseFailure)) throw error;
224
+ this.depth = 0;
225
+ recovering = true;
226
+ this.synchronize(failedAt);
227
+ }
228
+ }
229
+ return statements;
230
+ }
231
+
232
+ private parseStatement(): ast.Statement {
233
+ const token = this.current;
234
+ switch (token.type) {
235
+ case TokenType.SEMICOLON:
236
+ this.advance();
237
+ return this.parseStatementAfterStraySemicolon();
238
+ case TokenType.LET:
239
+ return this.endSimpleStatement(this.parseVariableDeclaration());
240
+ case TokenType.RETURN:
241
+ return this.endSimpleStatement(this.parseReturn());
242
+ case TokenType.IF:
243
+ return this.endCompoundStatement(this.parseIf());
244
+ case TokenType.WHILE:
245
+ return this.endCompoundStatement(this.parseWhile());
246
+ case TokenType.DO:
247
+ return this.endSimpleStatement(this.parseDoUntil());
248
+ case TokenType.FOR:
249
+ return this.endCompoundStatement(this.parseFor());
250
+ case TokenType.LBRACE:
251
+ return this.endCompoundStatement(this.parseBlock('para abrir el bloque'));
252
+ case TokenType.ELSE:
253
+ case TokenType.ELSEIF:
254
+ this.error(
255
+ 'RAMA_SIN_SI',
256
+ `${describeToken(token)} tiene que ir justo después del \`}\` de un \`si\`.`,
257
+ token.span,
258
+ );
259
+ case TokenType.UNTIL:
260
+ this.error('HASTA_QUE_SIN_HACER', '`hasta_que` tiene que ir justo después del `}` de un `hacer`.', token.span);
261
+ case TokenType.IDENT: {
262
+ const typoHint = KEYWORD_LIKE_TYPOS[token.lexeme];
263
+ const next = this.peek();
264
+ if (
265
+ typoHint &&
266
+ (next.type === TokenType.LBRACE || next.type === TokenType.IF || next.type === TokenType.LPAREN)
267
+ ) {
268
+ this.error('PALABRA_DESCONOCIDA', `\`${token.lexeme}\` no es una palabra de Codexivo.`, token.span, typoHint);
269
+ }
270
+ if (ASSIGNMENT_OPERATORS[next.type]) {
271
+ return this.endSimpleStatement(this.parseAssignment());
272
+ }
273
+ return this.endSimpleStatement(this.parseExpressionStatement());
274
+ }
275
+ default:
276
+ return this.endSimpleStatement(this.parseExpressionStatement());
277
+ }
278
+ }
279
+
280
+ private parseStatementAfterStraySemicolon(): ast.Statement {
281
+ // `;;` o un `;` suelto no hacen nada; seguimos con lo que venga.
282
+ if (this.check(TokenType.EOF) || this.check(TokenType.RBRACE)) {
283
+ const span = this.previous.span;
284
+ return { kind: 'Block', id: this.id(), statements: [], span };
285
+ }
286
+ return this.parseStatement();
287
+ }
288
+
289
+ /** Después de una sentencia simple va `;`, el final de la línea, `}` o el final del programa. */
290
+ private endSimpleStatement<T extends ast.Statement>(statement: T): T {
291
+ if (this.match(TokenType.SEMICOLON)) return statement;
292
+ if (this.check(TokenType.RBRACE) || this.check(TokenType.EOF)) return statement;
293
+ if (this.current.span.start.line > this.previous.span.end.line) return statement;
294
+ this.failSilentlyOnIllegal();
295
+ this.error(
296
+ 'FIN_DE_SENTENCIA',
297
+ `Se esperaba \`;\` o un salto de línea antes de ${describeToken(this.current)}.`,
298
+ this.current.span,
299
+ 'Si querías combinar dos valores, falta un operador entre ellos.',
300
+ );
301
+ }
302
+
303
+ private endCompoundStatement<T extends ast.Statement>(statement: T): T {
304
+ this.match(TokenType.SEMICOLON);
305
+ return statement;
306
+ }
307
+
308
+ private parseVariableDeclaration(): ast.VariableDeclaration {
309
+ const start = this.advance().span.start; // variable
310
+ const name = this.parseBindingName('después de `variable`');
311
+ this.expect(
312
+ TokenType.ASSIGN,
313
+ `después de \`variable ${name.name}\``,
314
+ `Toda variable empieza con un valor: \`variable ${name.name} = 0\`.`,
315
+ );
316
+ const value = this.parseExpression();
317
+ if (value.kind === 'FunctionLiteral' && !value.name) value.name = name.name;
318
+ return { kind: 'VariableDeclaration', id: this.id(), name, value, span: this.spanFrom(start) };
319
+ }
320
+
321
+ private parseBindingName(context: string): ast.Identifier {
322
+ const token = this.current;
323
+ if (token.type !== TokenType.IDENT) {
324
+ this.failSilentlyOnIllegal();
325
+ if (token.lexeme && /^[a-z_]+$/.test(token.lexeme) && token.type !== TokenType.EOF) {
326
+ this.error(
327
+ 'PALABRA_RESERVADA',
328
+ `\`${token.lexeme}\` es una palabra reservada y no se puede usar como nombre.`,
329
+ token.span,
330
+ 'Elige otro nombre, por ejemplo agregando una palabra: `valor_inicial`.',
331
+ );
332
+ }
333
+ this.error(
334
+ 'NOMBRE_ESPERADO',
335
+ `Se esperaba un nombre ${context}, pero se encontró ${describeToken(token)}.`,
336
+ token.span,
337
+ );
338
+ }
339
+ this.advance();
340
+ return { kind: 'Identifier', id: this.id(), name: token.lexeme, span: token.span };
341
+ }
342
+
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 {
358
+ const start = this.current.span.start;
359
+ const expression = this.parseExpression();
360
+ if (ASSIGNMENT_OPERATORS[this.current.type]) {
361
+ this.error(
362
+ 'ASIGNACIÓN_INVÁLIDA',
363
+ 'Solo se le puede asignar un valor a una variable.',
364
+ 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
+ );
369
+ }
370
+ return { kind: 'ExpressionStatement', id: this.id(), expression, span: this.spanFrom(start) };
371
+ }
372
+
373
+ private parseReturn(): ast.Return {
374
+ const keyword = this.advance();
375
+ const endsHere =
376
+ this.check(TokenType.SEMICOLON) ||
377
+ this.check(TokenType.RBRACE) ||
378
+ this.check(TokenType.EOF) ||
379
+ this.current.span.start.line > keyword.span.end.line;
380
+ const value = endsHere ? undefined : this.parseExpression();
381
+ return { kind: 'Return', id: this.id(), value, span: this.spanFrom(keyword.span.start) };
382
+ }
383
+
384
+ private parseBlock(context: string): ast.Block {
385
+ const open = this.expect(TokenType.LBRACE, context, 'Los bloques de Codexivo siempre van entre llaves `{ }`.');
386
+ const statements = this.parseStatements(TokenType.RBRACE);
387
+ if (!this.check(TokenType.RBRACE)) {
388
+ this.error(
389
+ 'BLOQUE_SIN_CERRAR',
390
+ `Falta \`}\` para cerrar el bloque que empieza en la línea ${open.span.start.line}.`,
391
+ open.span,
392
+ );
393
+ }
394
+ this.advance();
395
+ return { kind: 'Block', id: this.id(), statements, span: this.spanFrom(open.span.start) };
396
+ }
397
+
398
+ private parseCondition(keyword: string): ast.Expression {
399
+ this.expect(
400
+ TokenType.LPAREN,
401
+ `después de \`${keyword}\``,
402
+ `La condición va entre paréntesis: \`${keyword} (x > 0)\`.`,
403
+ );
404
+ const condition = this.parseExpression();
405
+ this.expect(TokenType.RPAREN, `para cerrar la condición de \`${keyword}\``);
406
+ return condition;
407
+ }
408
+
409
+ private parseIf(): ast.If {
410
+ const start = this.current.span.start;
411
+ const branches: ast.IfBranch[] = [];
412
+ let keyword = 'si';
413
+ do {
414
+ const branchStart = this.advance().span.start; // si / pero_si
415
+ const condition = this.parseCondition(keyword);
416
+ const body = this.parseBlock(`después de la condición de \`${keyword}\``);
417
+ branches.push({ condition, body, span: this.spanFrom(branchStart) });
418
+ keyword = 'pero_si';
419
+ } while (this.check(TokenType.ELSEIF));
420
+
421
+ let alternative: ast.Block | undefined;
422
+ if (this.match(TokenType.ELSE)) {
423
+ if (this.check(TokenType.IF)) {
424
+ this.error(
425
+ 'SI_NO_SI',
426
+ 'Después de `si_no` va directamente un bloque.',
427
+ this.current.span,
428
+ 'Para otra condición usa `pero_si (…)`.',
429
+ );
430
+ }
431
+ alternative = this.parseBlock('después de `si_no`');
432
+ }
433
+ return { kind: 'If', id: this.id(), branches, alternative, span: this.spanFrom(start) };
434
+ }
435
+
436
+ private parseWhile(): ast.While {
437
+ const start = this.advance().span.start;
438
+ const condition = this.parseCondition('mientras');
439
+ const body = this.parseBlock('después de la condición de `mientras`');
440
+ return { kind: 'While', id: this.id(), condition, body, span: this.spanFrom(start) };
441
+ }
442
+
443
+ private parseDoUntil(): ast.DoUntil {
444
+ const start = this.advance().span.start;
445
+ const body = this.parseBlock('después de `hacer`');
446
+ this.expect(
447
+ TokenType.UNTIL,
448
+ 'después del bloque de `hacer`',
449
+ 'La forma completa es `hacer { … } hasta_que (condición);`.',
450
+ );
451
+ const condition = this.parseCondition('hasta_que');
452
+ return { kind: 'DoUntil', id: this.id(), body, condition, span: this.spanFrom(start) };
453
+ }
454
+
455
+ private parseFor(): ast.For {
456
+ const start = this.advance().span.start;
457
+ this.expect(
458
+ TokenType.LPAREN,
459
+ 'después de `para`',
460
+ 'La forma completa es `para (variable i = 0; i < 10; i += 1) { … }`.',
461
+ );
462
+
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();
468
+ }
469
+ this.expect(TokenType.SEMICOLON, 'después del inicio del `para`');
470
+
471
+ const condition = this.check(TokenType.SEMICOLON) ? undefined : this.parseExpression();
472
+ this.expect(TokenType.SEMICOLON, 'después de la condición del `para`');
473
+
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();
478
+ }
479
+ this.expect(TokenType.RPAREN, 'para cerrar el `para`');
480
+
481
+ const body = this.parseBlock('después de `para (…)`');
482
+ return { kind: 'For', id: this.id(), init, condition, update, body, span: this.spanFrom(start) };
483
+ }
484
+
485
+ // ── expresiones ────────────────────────────────────────────────────────────
486
+
487
+ private parseExpression(precedence: Precedence = Precedence.LOWEST): ast.Expression {
488
+ this.enter();
489
+ let left = this.parsePrefix();
490
+ while (precedence < (INFIX_PRECEDENCE[this.current.type] ?? Precedence.LOWEST)) {
491
+ left = this.parseInfix(left);
492
+ }
493
+ this.leave();
494
+ return left;
495
+ }
496
+
497
+ private parsePrefix(): ast.Expression {
498
+ const token = this.current;
499
+ switch (token.type) {
500
+ case TokenType.NUMBER:
501
+ this.advance();
502
+ return { kind: 'NumberLiteral', id: this.id(), value: token.value as number, span: token.span };
503
+ case TokenType.STRING:
504
+ this.advance();
505
+ return { kind: 'StringLiteral', id: this.id(), value: token.value as string, span: token.span };
506
+ case TokenType.TRUE:
507
+ case TokenType.FALSE:
508
+ this.advance();
509
+ return { kind: 'BooleanLiteral', id: this.id(), value: token.type === TokenType.TRUE, span: token.span };
510
+ case TokenType.NULL:
511
+ this.advance();
512
+ return { kind: 'NullLiteral', id: this.id(), span: token.span };
513
+ case TokenType.IDENT:
514
+ this.advance();
515
+ return { kind: 'Identifier', id: this.id(), name: token.lexeme, span: token.span };
516
+ case TokenType.LPAREN: {
517
+ this.advance();
518
+ const expression = this.parseExpression();
519
+ this.expect(TokenType.RPAREN, 'para cerrar el paréntesis');
520
+ return expression;
521
+ }
522
+ case TokenType.LBRACKET:
523
+ return this.parseList();
524
+ case TokenType.MINUS: {
525
+ this.advance();
526
+ const operand = this.parseExpression(Precedence.UNARY);
527
+ return { kind: 'Unary', id: this.id(), operator: '-', operand, span: this.spanFrom(token.span.start) };
528
+ }
529
+ case TokenType.NOT:
530
+ case TokenType.BANG: {
531
+ this.advance();
532
+ const operand = this.parseExpression(Precedence.NOT);
533
+ return { kind: 'Unary', id: this.id(), operator: 'no', operand, span: this.spanFrom(token.span.start) };
534
+ }
535
+ case TokenType.FUNCTION:
536
+ return this.parseFunction();
537
+ case TokenType.ILLEGAL:
538
+ throw new ParseFailure();
539
+ case TokenType.IF:
540
+ case TokenType.WHILE:
541
+ case TokenType.FOR:
542
+ case TokenType.DO:
543
+ case TokenType.LET:
544
+ case TokenType.RETURN:
545
+ this.error(
546
+ 'SENTENCIA_EN_EXPRESIÓN',
547
+ `${describeToken(token)} empieza una sentencia y no se puede usar dentro de una expresión.`,
548
+ token.span,
549
+ );
550
+ case TokenType.EOF:
551
+ this.error(
552
+ 'EXPRESIÓN_INCOMPLETA',
553
+ 'El programa terminó a mitad de una expresión.',
554
+ token.span,
555
+ 'Revisa que no falte un valor al final.',
556
+ );
557
+ default:
558
+ this.error(
559
+ 'EXPRESIÓN_ESPERADA',
560
+ `Se esperaba un valor, pero se encontró ${describeToken(token)}.`,
561
+ token.span,
562
+ /^[a-z_]+$/.test(token.lexeme)
563
+ ? `\`${token.lexeme}\` es una palabra reservada; no se puede usar como nombre de variable.`
564
+ : undefined,
565
+ );
566
+ }
567
+ }
568
+
569
+ private parseInfix(left: ast.Expression): ast.Expression {
570
+ const token = this.current;
571
+ const start = left.span.start;
572
+ switch (token.type) {
573
+ case TokenType.LPAREN: {
574
+ this.advance();
575
+ const args = this.parseList_(TokenType.RPAREN, 'para cerrar los argumentos');
576
+ return { kind: 'Call', id: this.id(), callee: left, args, span: this.spanFrom(start) };
577
+ }
578
+ case TokenType.LBRACKET: {
579
+ this.advance();
580
+ if (this.check(TokenType.RBRACKET)) {
581
+ this.error(
582
+ 'ÍNDICE_VACÍO',
583
+ 'Falta el índice entre los corchetes.',
584
+ this.current.span,
585
+ 'Por ejemplo: `lista[0]`.',
586
+ );
587
+ }
588
+ const index = this.parseExpression();
589
+ this.expect(TokenType.RBRACKET, 'para cerrar el índice');
590
+ return { kind: 'Index', id: this.id(), object: left, index, span: this.spanFrom(start) };
591
+ }
592
+ case TokenType.AND:
593
+ case TokenType.OR: {
594
+ this.advance();
595
+ const precedence = INFIX_PRECEDENCE[token.type]!;
596
+ const right = this.parseExpression(precedence);
597
+ const operator = token.type === TokenType.AND ? 'y' : 'o';
598
+ return { kind: 'Logical', id: this.id(), operator, left, right, span: this.spanFrom(start) };
599
+ }
600
+ default: {
601
+ this.advance();
602
+ const precedence = INFIX_PRECEDENCE[token.type]!;
603
+ const right = this.parseExpression(precedence);
604
+ const operator = BINARY_OPERATORS[token.type]!;
605
+ if (precedence === Precedence.COMPARE && INFIX_PRECEDENCE[this.current.type] === Precedence.COMPARE) {
606
+ this.error(
607
+ 'COMPARACIÓN_ENCADENADA',
608
+ 'No se pueden encadenar comparaciones.',
609
+ { start, end: this.current.span.end },
610
+ 'Une las dos comparaciones con `y`, por ejemplo: `1 < x y x < 5`.',
611
+ );
612
+ }
613
+ return { kind: 'Binary', id: this.id(), operator, left, right, span: this.spanFrom(start) };
614
+ }
615
+ }
616
+ }
617
+
618
+ private parseList(): ast.ListLiteral {
619
+ const start = this.advance().span.start;
620
+ const elements = this.parseList_(TokenType.RBRACKET, 'para cerrar la lista');
621
+ return { kind: 'ListLiteral', id: this.id(), elements, span: this.spanFrom(start) };
622
+ }
623
+
624
+ /** Expresiones separadas por comas hasta `closing`. Se permite una coma final. */
625
+ private parseList_(closing: TokenType, context: string): ast.Expression[] {
626
+ const items: ast.Expression[] = [];
627
+ while (!this.check(closing)) {
628
+ items.push(this.parseExpression());
629
+ if (!this.match(TokenType.COMMA)) break;
630
+ }
631
+ this.expect(closing, context);
632
+ return items;
633
+ }
634
+
635
+ private parseFunction(): ast.FunctionLiteral {
636
+ 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
+ }
646
+ this.expect(
647
+ TokenType.LPAREN,
648
+ 'después de `procedimiento`',
649
+ 'Los parámetros van entre paréntesis: `procedimiento(a, b) { … }`.',
650
+ );
651
+ const params: ast.Identifier[] = [];
652
+ const seen = new Set<string>();
653
+ while (!this.check(TokenType.RPAREN)) {
654
+ const param = this.parseBindingName('como parámetro');
655
+ if (seen.has(param.name)) {
656
+ this.error('PARÁMETRO_REPETIDO', `El parámetro \`${param.name}\` está repetido.`, param.span);
657
+ }
658
+ seen.add(param.name);
659
+ params.push(param);
660
+ if (!this.match(TokenType.COMMA)) break;
661
+ }
662
+ 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) };
665
+ }
666
+ }