codexivo 0.0.0-stage → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +159 -2
- package/dist/index.js +154 -0
- package/dist/index.js.map +30 -0
- package/dist/worker.js +153 -0
- package/dist/worker.js.map +25 -0
- package/docs/README.md +27 -0
- package/docs/arquitectura.md +174 -0
- package/docs/especificacion-v1.md +701 -0
- package/docs/guia-aprender-desde-el-codigo.md +91 -0
- package/docs/integracion.md +104 -0
- package/docs/lenguaje.md +537 -0
- package/docs/modo-mundo.md +128 -0
- package/docs/recetario.md +159 -0
- package/docs/repl.md +64 -0
- package/package.json +66 -4
- package/src/catalog.ts +256 -0
- package/src/challenge.ts +138 -0
- package/src/engine.ts +104 -0
- package/src/error-codes.ts +427 -0
- package/src/errors.ts +56 -0
- package/src/flow.ts +385 -0
- package/src/index.ts +81 -0
- package/src/printer.ts +133 -0
- package/src/runtime/builtins.ts +1426 -0
- package/src/runtime/compiler.ts +499 -0
- package/src/runtime/costs.ts +96 -0
- package/src/runtime/machine.ts +1521 -0
- package/src/runtime/prelude.ts +230 -0
- package/src/runtime/suggest.ts +33 -0
- package/src/runtime/values.ts +241 -0
- package/src/sandbox-host.ts +310 -0
- package/src/sandbox.ts +95 -0
- package/src/syntax/ast.ts +344 -0
- package/src/syntax/lexer.ts +266 -0
- package/src/syntax/parser.ts +1090 -0
- package/src/syntax/token.ts +215 -0
- package/src/worker.ts +18 -0
- package/src/world.ts +518 -0
|
@@ -0,0 +1,1090 @@
|
|
|
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
|
+
POWER,
|
|
35
|
+
CALL,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const INFIX_PRECEDENCE: Partial<Record<TokenType, Precedence>> = {
|
|
39
|
+
[TokenType.OR]: Precedence.OR,
|
|
40
|
+
[TokenType.AND]: Precedence.AND,
|
|
41
|
+
[TokenType.EQ]: Precedence.COMPARE,
|
|
42
|
+
[TokenType.NEQ]: Precedence.COMPARE,
|
|
43
|
+
[TokenType.LT]: Precedence.COMPARE,
|
|
44
|
+
[TokenType.GT]: Precedence.COMPARE,
|
|
45
|
+
[TokenType.LTE]: Precedence.COMPARE,
|
|
46
|
+
[TokenType.GTE]: Precedence.COMPARE,
|
|
47
|
+
[TokenType.PLUS]: Precedence.SUM,
|
|
48
|
+
[TokenType.MINUS]: Precedence.SUM,
|
|
49
|
+
[TokenType.STAR]: Precedence.PRODUCT,
|
|
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,
|
|
55
|
+
[TokenType.LPAREN]: Precedence.CALL,
|
|
56
|
+
[TokenType.LBRACKET]: Precedence.CALL,
|
|
57
|
+
[TokenType.DOT]: Precedence.CALL,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const BINARY_OPERATORS: Partial<Record<TokenType, ast.BinaryOperator>> = {
|
|
61
|
+
[TokenType.PLUS]: '+',
|
|
62
|
+
[TokenType.MINUS]: '-',
|
|
63
|
+
[TokenType.STAR]: '*',
|
|
64
|
+
[TokenType.SLASH]: '/',
|
|
65
|
+
[TokenType.PERCENT]: 'mod',
|
|
66
|
+
[TokenType.MOD]: 'mod',
|
|
67
|
+
[TokenType.DIV]: 'div',
|
|
68
|
+
[TokenType.CARET]: '^',
|
|
69
|
+
[TokenType.EQ]: '==',
|
|
70
|
+
[TokenType.NEQ]: '!=',
|
|
71
|
+
[TokenType.LT]: '<',
|
|
72
|
+
[TokenType.GT]: '>',
|
|
73
|
+
[TokenType.LTE]: '<=',
|
|
74
|
+
[TokenType.GTE]: '>=',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const ASSIGNMENT_OPERATORS: Partial<Record<TokenType, ast.AssignmentOperator>> = {
|
|
78
|
+
[TokenType.ASSIGN]: '=',
|
|
79
|
+
[TokenType.PLUS_ASSIGN]: '+=',
|
|
80
|
+
[TokenType.MINUS_ASSIGN]: '-=',
|
|
81
|
+
[TokenType.STAR_ASSIGN]: '*=',
|
|
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]: '--',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const STATEMENT_KEYWORDS = new Set([
|
|
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,
|
|
100
|
+
TokenType.RETURN,
|
|
101
|
+
TokenType.IF,
|
|
102
|
+
TokenType.WHILE,
|
|
103
|
+
TokenType.DO,
|
|
104
|
+
TokenType.FOR,
|
|
105
|
+
]);
|
|
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
|
+
|
|
138
|
+
const KEYWORD_LIKE_TYPOS: Readonly<Record<string, string>> = {
|
|
139
|
+
sino: 'En Codexivo se escribe `si_no`, con guion bajo.',
|
|
140
|
+
pero: 'En Codexivo se escribe `pero_si`, con guion bajo.',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const MAX_ERRORS = 50;
|
|
144
|
+
const MAX_DEPTH = 400;
|
|
145
|
+
|
|
146
|
+
/** Señal interna para abandonar la sentencia actual. El error, si lo hay, ya se registró. */
|
|
147
|
+
class ParseFailure {}
|
|
148
|
+
|
|
149
|
+
class Parser {
|
|
150
|
+
readonly errors: CodexivoError[] = [];
|
|
151
|
+
private position = 0;
|
|
152
|
+
private nextId = 1;
|
|
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;
|
|
161
|
+
|
|
162
|
+
constructor(private readonly tokens: Token[]) {}
|
|
163
|
+
|
|
164
|
+
parseProgram(): ast.Program {
|
|
165
|
+
const start = this.current.span.start;
|
|
166
|
+
const statements = this.parseStatements(TokenType.EOF);
|
|
167
|
+
return { kind: 'Program', id: this.id(), statements, span: { start, end: this.current.span.end } };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── utilidades ─────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
private get current(): Token {
|
|
173
|
+
return this.tokens[this.position] ?? this.tokens[this.tokens.length - 1]!;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private get previous(): Token {
|
|
177
|
+
return this.tokens[this.position - 1] ?? this.current;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private peek(ahead = 1): Token {
|
|
181
|
+
return this.tokens[this.position + ahead] ?? this.tokens[this.tokens.length - 1]!;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private id(): number {
|
|
185
|
+
return this.nextId++;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private advance(): Token {
|
|
189
|
+
const token = this.current;
|
|
190
|
+
if (token.type !== TokenType.EOF) this.position += 1;
|
|
191
|
+
return token;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private check(type: TokenType): boolean {
|
|
195
|
+
return this.current.type === type;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private match(type: TokenType): boolean {
|
|
199
|
+
if (!this.check(type)) return false;
|
|
200
|
+
this.advance();
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private spanFrom(start: Position): Span {
|
|
205
|
+
return { start, end: this.previous.span.end };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
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
|
+
}
|
|
212
|
+
throw new ParseFailure();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Un token ILLEGAL ya tiene su error léxico; no lo reportamos dos veces. */
|
|
216
|
+
private failSilentlyOnIllegal(): void {
|
|
217
|
+
if (this.check(TokenType.ILLEGAL)) throw new ParseFailure();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private expect(type: TokenType, context: string, hint?: string): Token {
|
|
221
|
+
if (this.check(type)) return this.advance();
|
|
222
|
+
this.failSilentlyOnIllegal();
|
|
223
|
+
const found = this.current;
|
|
224
|
+
let finalHint = hint;
|
|
225
|
+
if (type === TokenType.RPAREN && found.type === TokenType.ASSIGN) {
|
|
226
|
+
finalHint = 'Para comparar se usa `es_igual_a` (o `==`); un solo `=` sirve para asignar.';
|
|
227
|
+
}
|
|
228
|
+
this.error(
|
|
229
|
+
'TOKEN_ESPERADO',
|
|
230
|
+
`Se esperaba ${describeTokenType(type)} ${context}, pero se encontró ${describeToken(found)}.`,
|
|
231
|
+
found.span,
|
|
232
|
+
finalHint,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private enter(): void {
|
|
237
|
+
this.depth += 1;
|
|
238
|
+
if (this.depth > MAX_DEPTH) {
|
|
239
|
+
this.error(
|
|
240
|
+
'ANIDAMIENTO_EXCESIVO',
|
|
241
|
+
'El programa tiene demasiados niveles anidados.',
|
|
242
|
+
this.current.span,
|
|
243
|
+
'Divide la expresión en variables intermedias.',
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private leave(): void {
|
|
249
|
+
this.depth -= 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Salta tokens hasta un punto seguro para seguir analizando después de un error. */
|
|
253
|
+
private synchronize(failedAt: Token): void {
|
|
254
|
+
if (this.position === this.tokens.indexOf(failedAt) && !this.check(TokenType.EOF)) this.advance();
|
|
255
|
+
while (!this.check(TokenType.EOF)) {
|
|
256
|
+
if (this.previous.type === TokenType.SEMICOLON) return;
|
|
257
|
+
if (this.check(TokenType.RBRACE)) return;
|
|
258
|
+
if (STATEMENT_KEYWORDS.has(this.current.type)) return;
|
|
259
|
+
if (this.current.span.start.line > failedAt.span.start.line) return;
|
|
260
|
+
this.advance();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── sentencias ─────────────────────────────────────────────────────────────
|
|
265
|
+
|
|
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[] {
|
|
279
|
+
const statements: ast.Statement[] = [];
|
|
280
|
+
let recovering = false;
|
|
281
|
+
while (!this.check(terminator) && !this.check(TokenType.EOF)) {
|
|
282
|
+
const failedAt = this.current;
|
|
283
|
+
try {
|
|
284
|
+
if (terminator === TokenType.EOF && this.check(TokenType.RBRACE)) {
|
|
285
|
+
// Una `}` sobrante después de un error suele ser parte de lo que ya se reportó.
|
|
286
|
+
if (recovering) {
|
|
287
|
+
this.advance();
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
this.error('LLAVE_SOBRANTE', 'Sobra esta `}`: no hay ningún bloque abierto.', this.current.span);
|
|
291
|
+
}
|
|
292
|
+
statements.push(this.parseStatement());
|
|
293
|
+
recovering = false;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (!(error instanceof ParseFailure)) throw error;
|
|
296
|
+
this.depth = 0;
|
|
297
|
+
recovering = true;
|
|
298
|
+
this.synchronize(failedAt);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
this.checkDuplicateProcedures(statements);
|
|
302
|
+
return statements;
|
|
303
|
+
}
|
|
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
|
+
|
|
333
|
+
private parseStatement(): ast.Statement {
|
|
334
|
+
const token = this.current;
|
|
335
|
+
switch (token.type) {
|
|
336
|
+
case TokenType.SEMICOLON:
|
|
337
|
+
this.advance();
|
|
338
|
+
return this.parseStatementAfterStraySemicolon();
|
|
339
|
+
case TokenType.LET:
|
|
340
|
+
case TokenType.CONST:
|
|
341
|
+
return this.endSimpleStatement(this.parseVariableDeclaration());
|
|
342
|
+
case TokenType.RETURN:
|
|
343
|
+
return this.endSimpleStatement(this.parseReturn());
|
|
344
|
+
case TokenType.IF:
|
|
345
|
+
return this.endCompoundStatement(this.parseIf());
|
|
346
|
+
case TokenType.WHILE:
|
|
347
|
+
return this.endCompoundStatement(this.parseWhile());
|
|
348
|
+
case TokenType.DO:
|
|
349
|
+
return this.endSimpleStatement(this.parseDoUntil());
|
|
350
|
+
case TokenType.FOR:
|
|
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());
|
|
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
|
+
}
|
|
378
|
+
return this.endCompoundStatement(this.parseBlock('para abrir el bloque'));
|
|
379
|
+
case TokenType.ELSE:
|
|
380
|
+
case TokenType.ELSEIF:
|
|
381
|
+
this.error(
|
|
382
|
+
'RAMA_SIN_SI',
|
|
383
|
+
`${describeToken(token)} tiene que ir justo después del \`}\` de un \`si\`.`,
|
|
384
|
+
token.span,
|
|
385
|
+
);
|
|
386
|
+
case TokenType.UNTIL:
|
|
387
|
+
this.error('HASTA_QUE_SIN_HACER', '`hasta_que` tiene que ir justo después del `}` de un `hacer`.', token.span);
|
|
388
|
+
case TokenType.IDENT: {
|
|
389
|
+
const typoHint = KEYWORD_LIKE_TYPOS[token.lexeme];
|
|
390
|
+
const next = this.peek();
|
|
391
|
+
if (
|
|
392
|
+
typoHint &&
|
|
393
|
+
(next.type === TokenType.LBRACE || next.type === TokenType.IF || next.type === TokenType.LPAREN)
|
|
394
|
+
) {
|
|
395
|
+
this.error('PALABRA_DESCONOCIDA', `\`${token.lexeme}\` no es una palabra de Codexivo.`, token.span, typoHint);
|
|
396
|
+
}
|
|
397
|
+
return this.endSimpleStatement(this.parseSimpleStatement());
|
|
398
|
+
}
|
|
399
|
+
default:
|
|
400
|
+
return this.endSimpleStatement(this.parseSimpleStatement());
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private parseStatementAfterStraySemicolon(): ast.Statement {
|
|
405
|
+
// `;;` o un `;` suelto no hacen nada; seguimos con lo que venga.
|
|
406
|
+
if (this.check(TokenType.EOF) || this.check(TokenType.RBRACE)) {
|
|
407
|
+
const span = this.previous.span;
|
|
408
|
+
return { kind: 'Block', id: this.id(), statements: [], span };
|
|
409
|
+
}
|
|
410
|
+
return this.parseStatement();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Después de una sentencia simple va `;`, el final de la línea, `}` o el final del programa. */
|
|
414
|
+
private endSimpleStatement<T extends ast.Statement>(statement: T): T {
|
|
415
|
+
if (this.match(TokenType.SEMICOLON)) return statement;
|
|
416
|
+
if (this.check(TokenType.RBRACE) || this.check(TokenType.EOF)) return statement;
|
|
417
|
+
if (this.current.span.start.line > this.previous.span.end.line) return statement;
|
|
418
|
+
this.failSilentlyOnIllegal();
|
|
419
|
+
this.error(
|
|
420
|
+
'FIN_DE_SENTENCIA',
|
|
421
|
+
`Se esperaba \`;\` o un salto de línea antes de ${describeToken(this.current)}.`,
|
|
422
|
+
this.current.span,
|
|
423
|
+
'Si querías combinar dos valores, falta un operador entre ellos.',
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private endCompoundStatement<T extends ast.Statement>(statement: T): T {
|
|
428
|
+
this.match(TokenType.SEMICOLON);
|
|
429
|
+
return statement;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private parseVariableDeclaration(): ast.VariableDeclaration {
|
|
433
|
+
const keyword = this.advance(); // variable / constante
|
|
434
|
+
const constant = keyword.type === TokenType.CONST;
|
|
435
|
+
const name = this.parseBindingName(`después de \`${keyword.lexeme}\``);
|
|
436
|
+
this.expect(
|
|
437
|
+
TokenType.ASSIGN,
|
|
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\`.`,
|
|
442
|
+
);
|
|
443
|
+
const value = this.parseExpression();
|
|
444
|
+
if (value.kind === 'FunctionLiteral' && !value.name) value.name = name.name;
|
|
445
|
+
return {
|
|
446
|
+
kind: 'VariableDeclaration',
|
|
447
|
+
id: this.id(),
|
|
448
|
+
constant,
|
|
449
|
+
name,
|
|
450
|
+
value,
|
|
451
|
+
span: this.spanFrom(keyword.span.start),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private parseBindingName(context: string): ast.Identifier {
|
|
456
|
+
const token = this.current;
|
|
457
|
+
if (token.type !== TokenType.IDENT) {
|
|
458
|
+
this.failSilentlyOnIllegal();
|
|
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();
|
|
462
|
+
this.error(
|
|
463
|
+
'PALABRA_RESERVADA',
|
|
464
|
+
`\`${token.lexeme}\` es una palabra reservada y no se puede usar como nombre.`,
|
|
465
|
+
token.span,
|
|
466
|
+
'Elige otro nombre, por ejemplo agregando una palabra: `valor_inicial`.',
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
this.error(
|
|
470
|
+
'NOMBRE_ESPERADO',
|
|
471
|
+
`Se esperaba un nombre ${context}, pero se encontró ${describeToken(token)}.`,
|
|
472
|
+
token.span,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
this.advance();
|
|
476
|
+
return { kind: 'Identifier', id: this.id(), name: token.lexeme, span: token.span };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Asignación (`x = …`, `lista[i] += …`), incremento (`i++`) o expresión. */
|
|
480
|
+
private parseSimpleStatement(): ast.Assignment | ast.Increment | ast.ExpressionStatement {
|
|
481
|
+
const start = this.current.span.start;
|
|
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') {
|
|
489
|
+
this.error(
|
|
490
|
+
'ASIGNACIÓN_INVÁLIDA',
|
|
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`.',
|
|
494
|
+
this.current.span,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
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
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private parseReturn(): ast.Return {
|
|
516
|
+
const keyword = this.advance();
|
|
517
|
+
const endsHere =
|
|
518
|
+
this.check(TokenType.SEMICOLON) ||
|
|
519
|
+
this.check(TokenType.RBRACE) ||
|
|
520
|
+
this.check(TokenType.EOF) ||
|
|
521
|
+
this.current.span.start.line > keyword.span.end.line;
|
|
522
|
+
const value = endsHere ? undefined : this.parseExpression();
|
|
523
|
+
return { kind: 'Return', id: this.id(), value, span: this.spanFrom(keyword.span.start) };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private parseBlock(context: string): ast.Block {
|
|
527
|
+
const open = this.expect(TokenType.LBRACE, context, 'Los bloques de Codexivo siempre van entre llaves `{ }`.');
|
|
528
|
+
const statements = this.parseStatements(TokenType.RBRACE);
|
|
529
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
530
|
+
this.error(
|
|
531
|
+
'BLOQUE_SIN_CERRAR',
|
|
532
|
+
`Falta \`}\` para cerrar el bloque que empieza en la línea ${open.span.start.line}.`,
|
|
533
|
+
open.span,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
this.advance();
|
|
537
|
+
return { kind: 'Block', id: this.id(), statements, span: this.spanFrom(open.span.start) };
|
|
538
|
+
}
|
|
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
|
+
|
|
550
|
+
private parseCondition(keyword: string): ast.Expression {
|
|
551
|
+
this.expect(
|
|
552
|
+
TokenType.LPAREN,
|
|
553
|
+
`después de \`${keyword}\``,
|
|
554
|
+
`La condición va entre paréntesis: \`${keyword} (x > 0)\`.`,
|
|
555
|
+
);
|
|
556
|
+
const condition = this.grouped(() => this.parseExpression());
|
|
557
|
+
this.expect(TokenType.RPAREN, `para cerrar la condición de \`${keyword}\``);
|
|
558
|
+
return condition;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private parseIf(): ast.If {
|
|
562
|
+
const start = this.current.span.start;
|
|
563
|
+
const branches: ast.IfBranch[] = [];
|
|
564
|
+
let keyword = 'si';
|
|
565
|
+
do {
|
|
566
|
+
const branchStart = this.advance().span.start; // si / pero_si
|
|
567
|
+
const condition = this.parseCondition(keyword);
|
|
568
|
+
const body = this.parseBlock(`después de la condición de \`${keyword}\``);
|
|
569
|
+
branches.push({ condition, body, span: this.spanFrom(branchStart) });
|
|
570
|
+
keyword = 'pero_si';
|
|
571
|
+
} while (this.check(TokenType.ELSEIF));
|
|
572
|
+
|
|
573
|
+
let alternative: ast.Block | undefined;
|
|
574
|
+
if (this.match(TokenType.ELSE)) {
|
|
575
|
+
if (this.check(TokenType.IF)) {
|
|
576
|
+
this.error(
|
|
577
|
+
'SI_NO_SI',
|
|
578
|
+
'Después de `si_no` va directamente un bloque.',
|
|
579
|
+
this.current.span,
|
|
580
|
+
'Para otra condición usa `pero_si (…)`.',
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
alternative = this.parseBlock('después de `si_no`');
|
|
584
|
+
}
|
|
585
|
+
return { kind: 'If', id: this.id(), branches, alternative, span: this.spanFrom(start) };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private parseWhile(): ast.While {
|
|
589
|
+
const start = this.advance().span.start;
|
|
590
|
+
const condition = this.parseCondition('mientras');
|
|
591
|
+
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) };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private parseDoUntil(): ast.DoUntil {
|
|
596
|
+
const start = this.advance().span.start;
|
|
597
|
+
const body = this.parseLoopBody('después de `hacer`');
|
|
598
|
+
this.expect(
|
|
599
|
+
TokenType.UNTIL,
|
|
600
|
+
'después del bloque de `hacer`',
|
|
601
|
+
'La forma completa es `hacer { … } hasta_que (condición)`.',
|
|
602
|
+
);
|
|
603
|
+
const condition = this.parseCondition('hasta_que');
|
|
604
|
+
return { kind: 'DoUntil', id: this.id(), body, condition, span: this.spanFrom(start) };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private parseFor(): ast.For | ast.ForRange {
|
|
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);
|
|
611
|
+
this.expect(
|
|
612
|
+
TokenType.LPAREN,
|
|
613
|
+
'después de `para`',
|
|
614
|
+
'La forma completa es `para (variable i = 0; i < 10; i += 1) { … }`.',
|
|
615
|
+
);
|
|
616
|
+
|
|
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
|
+
);
|
|
649
|
+
}
|
|
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
|
+
}
|
|
674
|
+
|
|
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
|
+
}
|
|
694
|
+
|
|
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;
|
|
711
|
+
}
|
|
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
|
+
}
|
|
737
|
+
|
|
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 };
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ── expresiones ────────────────────────────────────────────────────────────
|
|
794
|
+
|
|
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 {
|
|
797
|
+
this.enter();
|
|
798
|
+
let left = this.parsePrefix();
|
|
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;
|
|
802
|
+
left = this.parseInfix(left);
|
|
803
|
+
}
|
|
804
|
+
if (!statement && INCREMENT_OPERATORS[this.current.type]) this.incrementInExpression(left);
|
|
805
|
+
this.leave();
|
|
806
|
+
return left;
|
|
807
|
+
}
|
|
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
|
+
|
|
822
|
+
private parsePrefix(): ast.Expression {
|
|
823
|
+
const token = this.current;
|
|
824
|
+
switch (token.type) {
|
|
825
|
+
case TokenType.NUMBER:
|
|
826
|
+
this.advance();
|
|
827
|
+
return { kind: 'NumberLiteral', id: this.id(), value: token.value as number, span: token.span };
|
|
828
|
+
case TokenType.STRING:
|
|
829
|
+
this.advance();
|
|
830
|
+
return { kind: 'StringLiteral', id: this.id(), value: token.value as string, span: token.span };
|
|
831
|
+
case TokenType.TRUE:
|
|
832
|
+
case TokenType.FALSE:
|
|
833
|
+
this.advance();
|
|
834
|
+
return { kind: 'BooleanLiteral', id: this.id(), value: token.type === TokenType.TRUE, span: token.span };
|
|
835
|
+
case TokenType.NULL:
|
|
836
|
+
this.advance();
|
|
837
|
+
return { kind: 'NullLiteral', id: this.id(), span: token.span };
|
|
838
|
+
case TokenType.IDENT:
|
|
839
|
+
this.advance();
|
|
840
|
+
return { kind: 'Identifier', id: this.id(), name: token.lexeme, span: token.span };
|
|
841
|
+
case TokenType.LPAREN: {
|
|
842
|
+
this.advance();
|
|
843
|
+
const expression = this.grouped(() => this.parseExpression());
|
|
844
|
+
this.expect(TokenType.RPAREN, 'para cerrar el paréntesis');
|
|
845
|
+
return expression;
|
|
846
|
+
}
|
|
847
|
+
case TokenType.LBRACKET:
|
|
848
|
+
return this.parseList();
|
|
849
|
+
case TokenType.LBRACE:
|
|
850
|
+
return this.parseDict();
|
|
851
|
+
case TokenType.MINUS: {
|
|
852
|
+
this.advance();
|
|
853
|
+
const operand = this.parseExpression(Precedence.UNARY);
|
|
854
|
+
return { kind: 'Unary', id: this.id(), operator: '-', operand, span: this.spanFrom(token.span.start) };
|
|
855
|
+
}
|
|
856
|
+
case TokenType.NOT:
|
|
857
|
+
case TokenType.BANG: {
|
|
858
|
+
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) };
|
|
861
|
+
}
|
|
862
|
+
case TokenType.FUNCTION:
|
|
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();
|
|
884
|
+
case TokenType.ILLEGAL:
|
|
885
|
+
throw new ParseFailure();
|
|
886
|
+
case TokenType.IF:
|
|
887
|
+
case TokenType.WHILE:
|
|
888
|
+
case TokenType.FOR:
|
|
889
|
+
case TokenType.DO:
|
|
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:
|
|
897
|
+
case TokenType.RETURN:
|
|
898
|
+
this.error(
|
|
899
|
+
'SENTENCIA_EN_EXPRESIÓN',
|
|
900
|
+
`${describeToken(token)} empieza una sentencia y no se puede usar dentro de una expresión.`,
|
|
901
|
+
token.span,
|
|
902
|
+
);
|
|
903
|
+
case TokenType.EOF:
|
|
904
|
+
this.error(
|
|
905
|
+
'EXPRESIÓN_INCOMPLETA',
|
|
906
|
+
'El programa terminó a mitad de una expresión.',
|
|
907
|
+
token.span,
|
|
908
|
+
'Revisa que no falte un valor al final.',
|
|
909
|
+
);
|
|
910
|
+
default:
|
|
911
|
+
this.error(
|
|
912
|
+
'EXPRESIÓN_ESPERADA',
|
|
913
|
+
`Se esperaba un valor, pero se encontró ${describeToken(token)}.`,
|
|
914
|
+
token.span,
|
|
915
|
+
/^[a-z_]+$/.test(token.lexeme)
|
|
916
|
+
? `\`${token.lexeme}\` es una palabra reservada; no se puede usar como nombre de variable.`
|
|
917
|
+
: undefined,
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
private parseInfix(left: ast.Expression): ast.Expression {
|
|
923
|
+
const token = this.current;
|
|
924
|
+
const start = left.span.start;
|
|
925
|
+
switch (token.type) {
|
|
926
|
+
case TokenType.LPAREN: {
|
|
927
|
+
this.advance();
|
|
928
|
+
const args = this.parseList_(TokenType.RPAREN, 'para cerrar los argumentos');
|
|
929
|
+
return { kind: 'Call', id: this.id(), callee: left, args, span: this.spanFrom(start) };
|
|
930
|
+
}
|
|
931
|
+
case TokenType.LBRACKET: {
|
|
932
|
+
this.advance();
|
|
933
|
+
if (this.check(TokenType.RBRACKET)) {
|
|
934
|
+
this.error(
|
|
935
|
+
'ÍNDICE_VACÍO',
|
|
936
|
+
'Falta el índice entre los corchetes.',
|
|
937
|
+
this.current.span,
|
|
938
|
+
'Por ejemplo: `lista[0]`.',
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
const index = this.grouped(() => this.parseExpression());
|
|
942
|
+
this.expect(TokenType.RBRACKET, 'para cerrar el índice');
|
|
943
|
+
return { kind: 'Index', id: this.id(), object: left, index, span: this.spanFrom(start) };
|
|
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
|
+
}
|
|
968
|
+
case TokenType.AND:
|
|
969
|
+
case TokenType.OR: {
|
|
970
|
+
this.advance();
|
|
971
|
+
const precedence = INFIX_PRECEDENCE[token.type]!;
|
|
972
|
+
const right = this.parseExpression(precedence);
|
|
973
|
+
const operator = token.type === TokenType.AND ? 'y' : 'o';
|
|
974
|
+
return { kind: 'Logical', id: this.id(), operator, left, right, span: this.spanFrom(start) };
|
|
975
|
+
}
|
|
976
|
+
default: {
|
|
977
|
+
this.advance();
|
|
978
|
+
const precedence = INFIX_PRECEDENCE[token.type]!;
|
|
979
|
+
// `^` es asociativo a la derecha: `2 ^ 3 ^ 2` es `2 ^ (3 ^ 2)`.
|
|
980
|
+
const right = this.parseExpression(precedence === Precedence.POWER ? precedence - 1 : precedence);
|
|
981
|
+
const operator = BINARY_OPERATORS[token.type]!;
|
|
982
|
+
if (precedence === Precedence.COMPARE && INFIX_PRECEDENCE[this.current.type] === Precedence.COMPARE) {
|
|
983
|
+
this.error(
|
|
984
|
+
'COMPARACIÓN_ENCADENADA',
|
|
985
|
+
'No se pueden encadenar comparaciones.',
|
|
986
|
+
{ start, end: this.current.span.end },
|
|
987
|
+
'Une las dos comparaciones con `y`, por ejemplo: `1 < x y x < 5`.',
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
return { kind: 'Binary', id: this.id(), operator, left, right, span: this.spanFrom(start) };
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
private parseList(): ast.ListLiteral {
|
|
996
|
+
const start = this.advance().span.start;
|
|
997
|
+
const elements = this.parseList_(TokenType.RBRACKET, 'para cerrar la lista');
|
|
998
|
+
return { kind: 'ListLiteral', id: this.id(), elements, span: this.spanFrom(start) };
|
|
999
|
+
}
|
|
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
|
+
|
|
1040
|
+
/** Expresiones separadas por comas hasta `closing`. Se permite una coma final. */
|
|
1041
|
+
private parseList_(closing: TokenType, context: string): ast.Expression[] {
|
|
1042
|
+
const items: ast.Expression[] = [];
|
|
1043
|
+
while (!this.check(closing)) {
|
|
1044
|
+
items.push(this.grouped(() => this.parseExpression()));
|
|
1045
|
+
if (!this.match(TokenType.COMMA)) break;
|
|
1046
|
+
}
|
|
1047
|
+
this.expect(closing, context);
|
|
1048
|
+
return items;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
private parseFunctionDeclaration(): ast.FunctionDeclaration {
|
|
1052
|
+
const start = this.advance().span.start;
|
|
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 {
|
|
1060
|
+
this.expect(
|
|
1061
|
+
TokenType.LPAREN,
|
|
1062
|
+
'después de `procedimiento`',
|
|
1063
|
+
'Los parámetros van entre paréntesis: `procedimiento(a, b) { … }`.',
|
|
1064
|
+
);
|
|
1065
|
+
const params: ast.Identifier[] = [];
|
|
1066
|
+
const seen = new Set<string>();
|
|
1067
|
+
while (!this.check(TokenType.RPAREN)) {
|
|
1068
|
+
const param = this.parseBindingName('como parámetro');
|
|
1069
|
+
if (seen.has(param.name)) {
|
|
1070
|
+
this.error('PARÁMETRO_REPETIDO', `El parámetro \`${param.name}\` está repetido.`, param.span);
|
|
1071
|
+
}
|
|
1072
|
+
seen.add(param.name);
|
|
1073
|
+
params.push(param);
|
|
1074
|
+
if (!this.match(TokenType.COMMA)) break;
|
|
1075
|
+
}
|
|
1076
|
+
this.expect(TokenType.RPAREN, 'para cerrar los parámetros');
|
|
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) };
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const describeKey = (key: string | number): string => (typeof key === 'string' ? JSON.stringify(key) : String(key));
|