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.
@@ -0,0 +1,344 @@
1
+ import type { Span } from './token';
2
+
3
+ /**
4
+ * El AST está hecho de objetos planos: se puede serializar, enviar a un Web Worker y dibujar sin
5
+ * perder información. Cada nodo tiene un `id` único dentro de su programa y su rango en el código.
6
+ */
7
+ interface BaseNode {
8
+ id: number;
9
+ span: Span;
10
+ }
11
+
12
+ /** `%` y `mod` son el mismo operador; en el AST siempre aparece como `mod`. */
13
+ export type BinaryOperator = '+' | '-' | '*' | '/' | 'div' | 'mod' | '^' | '==' | '!=' | '<' | '>' | '<=' | '>=';
14
+ export type LogicalOperator = 'y' | 'o';
15
+ export type UnaryOperator = '-' | 'no';
16
+ export type AssignmentOperator = '=' | '+=' | '-=' | '*=' | '/=' | '%=';
17
+ export type IncrementOperator = '++' | '--';
18
+
19
+ // ── expresiones ──────────────────────────────────────────────────────────────
20
+
21
+ export interface NumberLiteral extends BaseNode {
22
+ kind: 'NumberLiteral';
23
+ value: number;
24
+ }
25
+
26
+ export interface StringLiteral extends BaseNode {
27
+ kind: 'StringLiteral';
28
+ value: string;
29
+ }
30
+
31
+ export interface BooleanLiteral extends BaseNode {
32
+ kind: 'BooleanLiteral';
33
+ value: boolean;
34
+ }
35
+
36
+ export interface NullLiteral extends BaseNode {
37
+ kind: 'NullLiteral';
38
+ }
39
+
40
+ export interface ListLiteral extends BaseNode {
41
+ kind: 'ListLiteral';
42
+ elements: Expression[];
43
+ }
44
+
45
+ export interface Identifier extends BaseNode {
46
+ kind: 'Identifier';
47
+ name: string;
48
+ }
49
+
50
+ export interface Unary extends BaseNode {
51
+ kind: 'Unary';
52
+ operator: UnaryOperator;
53
+ operand: Expression;
54
+ }
55
+
56
+ export interface Binary extends BaseNode {
57
+ kind: 'Binary';
58
+ operator: BinaryOperator;
59
+ left: Expression;
60
+ right: Expression;
61
+ }
62
+
63
+ /** `y` / `o`: se evalúan en cortocircuito, por eso no son un `Binary`. */
64
+ export interface Logical extends BaseNode {
65
+ kind: 'Logical';
66
+ operator: LogicalOperator;
67
+ left: Expression;
68
+ right: Expression;
69
+ }
70
+
71
+ export interface Call extends BaseNode {
72
+ kind: 'Call';
73
+ callee: Expression;
74
+ args: Expression[];
75
+ }
76
+
77
+ export interface Index extends BaseNode {
78
+ kind: 'Index';
79
+ object: Expression;
80
+ index: Expression;
81
+ /** `true` si se escribió `d.campo`; entonces `index` es el texto `"campo"`. */
82
+ dot?: boolean;
83
+ }
84
+
85
+ export interface DictEntry {
86
+ /** Rango de `clave: valor`. */
87
+ span: Span;
88
+ key: Expression;
89
+ value: Expression;
90
+ }
91
+
92
+ export interface DictLiteral extends BaseNode {
93
+ kind: 'DictLiteral';
94
+ entries: DictEntry[];
95
+ }
96
+
97
+ export interface FunctionLiteral extends BaseNode {
98
+ kind: 'FunctionLiteral';
99
+ /** Nombre con el que se declaró, si se conoce (`variable doble = procedimiento…` → `doble`). */
100
+ name?: string;
101
+ params: Identifier[];
102
+ body: Block;
103
+ }
104
+
105
+ export type Expression =
106
+ | NumberLiteral
107
+ | StringLiteral
108
+ | BooleanLiteral
109
+ | NullLiteral
110
+ | ListLiteral
111
+ | DictLiteral
112
+ | Identifier
113
+ | Unary
114
+ | Binary
115
+ | Logical
116
+ | Call
117
+ | Index
118
+ | FunctionLiteral;
119
+
120
+ // ── sentencias ───────────────────────────────────────────────────────────────
121
+
122
+ export interface VariableDeclaration extends BaseNode {
123
+ kind: 'VariableDeclaration';
124
+ /** `true` si se declaró con `constante`: no se puede reasignar. */
125
+ constant: boolean;
126
+ name: Identifier;
127
+ value: Expression;
128
+ }
129
+
130
+ /** Lo que puede ir a la izquierda de `=`: una variable o un elemento (`lista[i]`). */
131
+ export type AssignmentTarget = Identifier | Index;
132
+
133
+ export interface Assignment extends BaseNode {
134
+ kind: 'Assignment';
135
+ target: AssignmentTarget;
136
+ operator: AssignmentOperator;
137
+ value: Expression;
138
+ }
139
+
140
+ /** `i++` / `i--`: son sentencias, no expresiones. */
141
+ export interface Increment extends BaseNode {
142
+ kind: 'Increment';
143
+ target: AssignmentTarget;
144
+ operator: IncrementOperator;
145
+ }
146
+
147
+ /** `procedimiento nombre(…) { … }`: se puede usar desde el inicio de su bloque. */
148
+ export interface FunctionDeclaration extends BaseNode {
149
+ kind: 'FunctionDeclaration';
150
+ name: Identifier;
151
+ function: FunctionLiteral;
152
+ }
153
+
154
+ export interface ExpressionStatement extends BaseNode {
155
+ kind: 'ExpressionStatement';
156
+ expression: Expression;
157
+ }
158
+
159
+ export interface Return extends BaseNode {
160
+ kind: 'Return';
161
+ value?: Expression;
162
+ }
163
+
164
+ export interface Block extends BaseNode {
165
+ kind: 'Block';
166
+ statements: Statement[];
167
+ }
168
+
169
+ export interface IfBranch {
170
+ /** Rango de la rama completa: desde `si` / `pero_si` hasta su `}`. */
171
+ span: Span;
172
+ condition: Expression;
173
+ body: Block;
174
+ }
175
+
176
+ /** `si … pero_si … si_no`: las ramas se guardan en orden, no anidadas. */
177
+ export interface If extends BaseNode {
178
+ kind: 'If';
179
+ branches: IfBranch[];
180
+ alternative?: Block;
181
+ }
182
+
183
+ export interface While extends BaseNode {
184
+ kind: 'While';
185
+ condition: Expression;
186
+ body: Block;
187
+ }
188
+
189
+ /** `hacer { … } hasta_que (condición)`: repite el cuerpo hasta que la condición sea verdadera. */
190
+ export interface DoUntil extends BaseNode {
191
+ kind: 'DoUntil';
192
+ body: Block;
193
+ condition: Expression;
194
+ }
195
+
196
+ export interface For extends BaseNode {
197
+ kind: 'For';
198
+ init?: VariableDeclaration | Assignment | Increment | ExpressionStatement;
199
+ condition?: Expression;
200
+ update?: Assignment | Increment | ExpressionStatement;
201
+ body: Block;
202
+ }
203
+
204
+ /**
205
+ * `para i desde a hasta b con_paso p { … }`: cuenta de `a` a `b`, incluidos los dos. Los límites y el paso se
206
+ * evalúan una vez, al empezar; `i` es nueva en cada vuelta y no se puede cambiar dentro del cuerpo.
207
+ */
208
+ export interface ForRange extends BaseNode {
209
+ kind: 'ForRange';
210
+ variable: Identifier;
211
+ from: Expression;
212
+ to: Expression;
213
+ step?: Expression;
214
+ body: Block;
215
+ }
216
+
217
+ /** `repetir n veces { … }`: el contador es interno, no una variable visible. */
218
+ export interface Repeat extends BaseNode {
219
+ kind: 'Repeat';
220
+ count: Expression;
221
+ body: Block;
222
+ }
223
+
224
+ /** `para_cada x en lista { … }`: recorre una lista o un texto. */
225
+ export interface ForEach extends BaseNode {
226
+ kind: 'ForEach';
227
+ variable: Identifier;
228
+ /** Segunda variable de `para_cada clave, valor en diccionario`. */
229
+ value?: Identifier;
230
+ iterable: Expression;
231
+ body: Block;
232
+ }
233
+
234
+ /**
235
+ * `dominio nombre { … }`: un espacio de nombres. Agrupa declaraciones (variables, constantes, procedimientos y
236
+ * otros dominios) que se usan desde fuera con `nombre.miembro`. No es un módulo: no hay `importar`.
237
+ */
238
+ export interface Domain extends BaseNode {
239
+ kind: 'Domain';
240
+ name: Identifier;
241
+ body: Block;
242
+ }
243
+
244
+ export interface Break extends BaseNode {
245
+ kind: 'Break';
246
+ }
247
+
248
+ export interface Continue extends BaseNode {
249
+ kind: 'Continue';
250
+ }
251
+
252
+ export type Statement =
253
+ | VariableDeclaration
254
+ | FunctionDeclaration
255
+ | Assignment
256
+ | Increment
257
+ | ExpressionStatement
258
+ | Return
259
+ | Block
260
+ | If
261
+ | While
262
+ | DoUntil
263
+ | For
264
+ | ForRange
265
+ | ForEach
266
+ | Repeat
267
+ | Break
268
+ | Continue
269
+ | Domain;
270
+
271
+ export interface Program extends BaseNode {
272
+ kind: 'Program';
273
+ statements: Statement[];
274
+ }
275
+
276
+ export type Node = Program | Statement | Expression;
277
+
278
+ /** Hijos directos de un nodo, en el orden en que aparecen en el código. */
279
+ export const childrenOf = (node: Node): Node[] => {
280
+ switch (node.kind) {
281
+ case 'Program':
282
+ case 'Block':
283
+ return node.statements;
284
+ case 'ListLiteral':
285
+ return node.elements;
286
+ case 'DictLiteral':
287
+ return node.entries.flatMap(entry => [entry.key, entry.value]);
288
+ case 'Unary':
289
+ return [node.operand];
290
+ case 'Binary':
291
+ case 'Logical':
292
+ return [node.left, node.right];
293
+ case 'Call':
294
+ return [node.callee, ...node.args];
295
+ case 'Index':
296
+ return [node.object, node.index];
297
+ case 'FunctionLiteral':
298
+ return [...node.params, node.body];
299
+ case 'VariableDeclaration':
300
+ return [node.name, node.value];
301
+ case 'Assignment':
302
+ return [node.target, node.value];
303
+ case 'Increment':
304
+ return [node.target];
305
+ case 'FunctionDeclaration':
306
+ return [node.name, node.function];
307
+ case 'ForEach':
308
+ return [node.variable, ...(node.value ? [node.value] : []), node.iterable, node.body];
309
+ case 'ExpressionStatement':
310
+ return [node.expression];
311
+ case 'Return':
312
+ return node.value ? [node.value] : [];
313
+ case 'If':
314
+ return [
315
+ ...node.branches.flatMap(branch => [branch.condition, branch.body]),
316
+ ...(node.alternative ? [node.alternative] : []),
317
+ ];
318
+ case 'While':
319
+ return [node.condition, node.body];
320
+ case 'DoUntil':
321
+ return [node.body, node.condition];
322
+ case 'Domain':
323
+ return [node.name, node.body];
324
+ case 'ForRange':
325
+ return [node.variable, node.from, node.to, ...(node.step ? [node.step] : []), node.body];
326
+ case 'Repeat':
327
+ return [node.count, node.body];
328
+ case 'For':
329
+ return [
330
+ ...(node.init ? [node.init] : []),
331
+ ...(node.condition ? [node.condition] : []),
332
+ ...(node.update ? [node.update] : []),
333
+ node.body,
334
+ ];
335
+ default:
336
+ return [];
337
+ }
338
+ };
339
+
340
+ /** Recorre el árbol en profundidad, del padre a los hijos. */
341
+ export const walk = (node: Node, visit: (node: Node, parent?: Node) => void, parent?: Node): void => {
342
+ visit(node, parent);
343
+ for (const child of childrenOf(node)) walk(child, visit, node);
344
+ };
@@ -0,0 +1,266 @@
1
+ import { CodexivoError } from '../errors';
2
+ import { KEYWORDS, TokenType } from './token';
3
+ import type { Position, Span, Token } from './token';
4
+
5
+ export interface LexResult {
6
+ tokens: Token[];
7
+ errors: CodexivoError[];
8
+ }
9
+
10
+ const LETTER = /[A-Za-z_À-ÖØ-öø-ÿ]/;
11
+ const DIGIT = /[0-9]/;
12
+ const WHITESPACE = /\s/;
13
+
14
+ const TWO_CHAR_OPERATORS: Readonly<Record<string, TokenType>> = {
15
+ '==': TokenType.EQ,
16
+ '!=': TokenType.NEQ,
17
+ '<=': TokenType.LTE,
18
+ '>=': TokenType.GTE,
19
+ '+=': TokenType.PLUS_ASSIGN,
20
+ '-=': TokenType.MINUS_ASSIGN,
21
+ '*=': TokenType.STAR_ASSIGN,
22
+ '/=': TokenType.SLASH_ASSIGN,
23
+ '%=': TokenType.PERCENT_ASSIGN,
24
+ '++': TokenType.INCREMENT,
25
+ '--': TokenType.DECREMENT,
26
+ };
27
+
28
+ const ONE_CHAR_OPERATORS: Readonly<Record<string, TokenType>> = {
29
+ '=': TokenType.ASSIGN,
30
+ '+': TokenType.PLUS,
31
+ '-': TokenType.MINUS,
32
+ '*': TokenType.STAR,
33
+ '/': TokenType.SLASH,
34
+ '%': TokenType.PERCENT,
35
+ '^': TokenType.CARET,
36
+ '!': TokenType.BANG,
37
+ '<': TokenType.LT,
38
+ '>': TokenType.GT,
39
+ ',': TokenType.COMMA,
40
+ ':': TokenType.COLON,
41
+ '.': TokenType.DOT,
42
+ ';': TokenType.SEMICOLON,
43
+ '(': TokenType.LPAREN,
44
+ ')': TokenType.RPAREN,
45
+ '{': TokenType.LBRACE,
46
+ '}': TokenType.RBRACE,
47
+ '[': TokenType.LBRACKET,
48
+ ']': TokenType.RBRACKET,
49
+ };
50
+
51
+ const VALUE_ENDS = new Set([
52
+ TokenType.IDENT,
53
+ TokenType.NUMBER,
54
+ TokenType.STRING,
55
+ TokenType.RPAREN,
56
+ TokenType.RBRACKET,
57
+ TokenType.RBRACE,
58
+ ]);
59
+
60
+ const ESCAPES: Readonly<Record<string, string>> = {
61
+ n: '\n',
62
+ t: '\t',
63
+ '\\': '\\',
64
+ '"': '"',
65
+ "'": "'",
66
+ };
67
+
68
+ const ILLEGAL_HINTS: Readonly<Record<string, string>> = {
69
+ '&': 'Para el «y» lógico escribe la palabra `y`.',
70
+ '|': 'Para el «o» lógico escribe la palabra `o`.',
71
+ '“': 'Parece una comilla tipográfica; usa comillas rectas `"`.',
72
+ '”': 'Parece una comilla tipográfica; usa comillas rectas `"`.',
73
+ '‘': "Parece una comilla tipográfica; usa comillas rectas `'`.",
74
+ '’': "Parece una comilla tipográfica; usa comillas rectas `'`.",
75
+ };
76
+
77
+ /** Convierte código fuente en tokens. Nunca lanza: los problemas se reportan en `errors`. */
78
+ export const tokenize = (source: string): LexResult => {
79
+ const tokens: Token[] = [];
80
+ const errors: CodexivoError[] = [];
81
+
82
+ let offset = 0;
83
+ let line = 1;
84
+ let column = 1;
85
+
86
+ const position = (): Position => ({ line, column, offset });
87
+ const peek = (ahead = 0): string => source[offset + ahead] ?? '';
88
+ const advance = (): string => {
89
+ const character = source[offset] ?? '';
90
+ offset += 1;
91
+ if (character === '\n') {
92
+ line += 1;
93
+ column = 1;
94
+ } else {
95
+ column += 1;
96
+ }
97
+ return character;
98
+ };
99
+
100
+ const push = (type: TokenType, start: Position, value?: number | string): Token => {
101
+ const token: Token = { type, lexeme: source.slice(start.offset, offset), span: { start, end: position() } };
102
+ if (value !== undefined) token.value = value;
103
+ tokens.push(token);
104
+ return token;
105
+ };
106
+
107
+ const lexicalError = (code: string, message: string, span: Span, hint?: string) => {
108
+ errors.push(new CodexivoError({ kind: 'léxico', code, message, hint, span }));
109
+ };
110
+
111
+ while (offset < source.length) {
112
+ const character = peek();
113
+
114
+ if (WHITESPACE.test(character)) {
115
+ advance();
116
+ continue;
117
+ }
118
+
119
+ const start = position();
120
+
121
+ // comentarios
122
+ if (character === '/' && peek(1) === '/') {
123
+ while (offset < source.length && peek() !== '\n') advance();
124
+ continue;
125
+ }
126
+ if (character === '/' && peek(1) === '*') {
127
+ advance();
128
+ advance();
129
+ while (offset < source.length && !(peek() === '*' && peek(1) === '/')) advance();
130
+ if (offset >= source.length) {
131
+ lexicalError(
132
+ 'COMENTARIO_SIN_CERRAR',
133
+ 'El comentario que empieza aquí no se cerró.',
134
+ {
135
+ start,
136
+ end: { line: start.line, column: start.column + 2, offset: start.offset + 2 },
137
+ },
138
+ 'Cierra el comentario con `*/`.',
139
+ );
140
+ } else {
141
+ advance();
142
+ advance();
143
+ }
144
+ continue;
145
+ }
146
+
147
+ // números
148
+ // Después de un valor, `.` es acceso a un campo (`d.1` es un error claro, no `d` seguido de `.1`).
149
+ const last = tokens[tokens.length - 1];
150
+ const afterValue = last !== undefined && VALUE_ENDS.has(last.type) && last.span.end.offset === offset;
151
+ if (DIGIT.test(character) || (character === '.' && DIGIT.test(peek(1)) && !afterValue)) {
152
+ // `_` separa grupos de dígitos (`1_000_000`); se revisa que esté bien puesto al final.
153
+ const digits = () => {
154
+ while (DIGIT.test(peek()) || peek() === '_') advance();
155
+ };
156
+ digits();
157
+ if (peek() === '.' && DIGIT.test(peek(1))) {
158
+ advance();
159
+ digits();
160
+ }
161
+ if (LETTER.test(peek()) && peek() !== '_') {
162
+ while (LETTER.test(peek()) || DIGIT.test(peek())) advance();
163
+ const token = push(TokenType.ILLEGAL, start);
164
+ lexicalError(
165
+ 'NOMBRE_EMPIEZA_CON_NÚMERO',
166
+ `\`${token.lexeme}\` no es un número ni un nombre válido.`,
167
+ token.span,
168
+ 'Los nombres no pueden empezar con un dígito. Si es una multiplicación, escribe el operador: `2 * x`.',
169
+ );
170
+ continue;
171
+ }
172
+ const text = source.slice(start.offset, offset);
173
+ if (/(^|\D)_|_(\D|$)/.test(text)) {
174
+ const token = push(TokenType.ILLEGAL, start);
175
+ lexicalError(
176
+ 'SEPARADOR_INVÁLIDO',
177
+ `\`${text}\` no es un número válido.`,
178
+ token.span,
179
+ 'El `_` solo puede ir entre dos dígitos para separar grupos, como en `1_000_000`.',
180
+ );
181
+ continue;
182
+ }
183
+ push(TokenType.NUMBER, start, Number(text.replaceAll('_', '')));
184
+ continue;
185
+ }
186
+
187
+ // nombres y palabras reservadas
188
+ if (LETTER.test(character)) {
189
+ while (LETTER.test(peek()) || DIGIT.test(peek())) advance();
190
+ const word = source.slice(start.offset, offset);
191
+ push(KEYWORDS[word] ?? TokenType.IDENT, start);
192
+ continue;
193
+ }
194
+
195
+ // textos
196
+ if (character === '"' || character === "'") {
197
+ const quote = advance();
198
+ let value = '';
199
+ let closed = false;
200
+ while (offset < source.length && peek() !== '\n') {
201
+ const current = advance();
202
+ if (current === quote) {
203
+ closed = true;
204
+ break;
205
+ }
206
+ if (current === '\\') {
207
+ const escapeStart = { line, column: column - 1, offset: offset - 1 };
208
+ const escaped = advance();
209
+ const replacement = ESCAPES[escaped];
210
+ if (replacement === undefined) {
211
+ lexicalError(
212
+ 'ESCAPE_DESCONOCIDO',
213
+ `\`\\${escaped}\` no es una secuencia de escape válida.`,
214
+ { start: escapeStart, end: position() },
215
+ 'Las secuencias válidas son `\\n`, `\\t`, `\\\\`, `\\"` y `\\\'`.',
216
+ );
217
+ value += escaped;
218
+ } else {
219
+ value += replacement;
220
+ }
221
+ continue;
222
+ }
223
+ value += current;
224
+ }
225
+ const token = push(TokenType.STRING, start, value);
226
+ if (!closed) {
227
+ lexicalError(
228
+ 'TEXTO_SIN_CERRAR',
229
+ 'El texto que empieza aquí no se cerró.',
230
+ token.span,
231
+ `Agrega la comilla \`${quote}\` al final. Un texto no puede ocupar varias líneas; usa \`\\n\` para un salto de línea.`,
232
+ );
233
+ }
234
+ continue;
235
+ }
236
+
237
+ // operadores y delimitadores
238
+ const pair = character + peek(1);
239
+ const twoChar = TWO_CHAR_OPERATORS[pair];
240
+ if (twoChar) {
241
+ advance();
242
+ advance();
243
+ push(twoChar, start);
244
+ continue;
245
+ }
246
+ const oneChar = ONE_CHAR_OPERATORS[character];
247
+ if (oneChar) {
248
+ advance();
249
+ push(oneChar, start);
250
+ continue;
251
+ }
252
+
253
+ advance();
254
+ const token = push(TokenType.ILLEGAL, start);
255
+ lexicalError(
256
+ 'CARÁCTER_DESCONOCIDO',
257
+ `Codexivo no reconoce el carácter \`${character}\`.`,
258
+ token.span,
259
+ ILLEGAL_HINTS[character],
260
+ );
261
+ }
262
+
263
+ const end = position();
264
+ tokens.push({ type: TokenType.EOF, lexeme: '', span: { start: end, end } });
265
+ return { tokens, errors };
266
+ };