codexivo 0.2.3 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/dist/index.js +27 -25
- package/dist/index.js.map +8 -8
- package/dist/worker.js +29 -27
- package/dist/worker.js.map +6 -6
- package/docs/arquitectura.md +46 -1
- package/docs/especificacion-v1.md +19 -14
- package/docs/integracion.md +1 -0
- package/docs/lenguaje.md +22 -8
- package/package.json +1 -1
- package/src/catalog.ts +1 -0
- package/src/error-codes.ts +5 -0
- package/src/index.ts +2 -2
- package/src/runtime/builtins.ts +44 -0
- package/src/runtime/values.ts +1 -0
- package/src/sandbox-host.ts +15 -2
- package/src/syntax/parser.ts +295 -87
package/docs/arquitectura.md
CHANGED
|
@@ -29,6 +29,51 @@ código ──tokenize──▶ tokens ──parse──▶ AST ──compile─
|
|
|
29
29
|
- Ante un error, el parser lo registra, salta hasta un punto seguro (un `;`, una `}`, una palabra que empieza sentencia o una
|
|
30
30
|
línea nueva) y sigue. Así reporta varios errores en una pasada sin inventar errores en cascada.
|
|
31
31
|
|
|
32
|
+
### Traza del parser
|
|
33
|
+
|
|
34
|
+
`parse(código, { trace: true })` (o `traceParse(código)`) regresa además `trace`: los pasos con los que el parser construyó
|
|
35
|
+
el árbol, en orden. Con eso el sitio puede animar **cómo** se construye, no solo mostrar el resultado. Cada evento es un
|
|
36
|
+
objeto plano con su `span`:
|
|
37
|
+
|
|
38
|
+
| Evento | Qué cuenta |
|
|
39
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| `consume` | El parser toma un token: `index` en `tokens`, `tokenType` y `lexeme`. |
|
|
41
|
+
| `enter` | Empieza a construir un nodo (`kind`). Si lo construye a partir de uno ya hecho, `left` es el `id` de ese nodo. |
|
|
42
|
+
| `exit` | Termina el nodo: `kind` e `id`, el mismo `id` que tiene en el AST. |
|
|
43
|
+
| `precedence` | Decide de quién es un valor (ver abajo). |
|
|
44
|
+
| `error` | Un error de sintaxis, con su `code` y `message` (el mismo que está en `errors`). |
|
|
45
|
+
| `recover` | Después de un error: qué nodos abiertos abandona y qué tokens salta para seguir. |
|
|
46
|
+
|
|
47
|
+
**Nodos abiertos.** Entre el `enter` y el `exit` de un nodo, el nodo está abierto esperando a sus hijos, que son los nodos
|
|
48
|
+
que terminan en medio. Un parser Pratt no sabe que `1` es el lado izquierdo de una suma hasta que ve el `+`: por eso el
|
|
49
|
+
`enter` del `Binary` llega después del `exit` del `1` y lleva `left` con su `id`; la vista mueve ese nodo dentro del nuevo.
|
|
50
|
+
Lo mismo pasa con las llamadas (`f` en `f(1)`), los índices, las asignaciones (`x` en `x = 1`) y las sentencias que son
|
|
51
|
+
una expresión.
|
|
52
|
+
|
|
53
|
+
**Precedencia.** Es la decisión central del parser. En `1 + 2 * 3`, después de leer `2` el parser está construyendo el lado
|
|
54
|
+
derecho del `+` y llega el `*`. El evento lleva:
|
|
55
|
+
|
|
56
|
+
- `operand`: el `id` del valor en disputa (el `2`);
|
|
57
|
+
- `building`: el operador que está esperando ese valor (`+`) y la precedencia con la que compara; no viene si ningún
|
|
58
|
+
operador lo espera (al inicio de una expresión o dentro de un paréntesis);
|
|
59
|
+
- `incoming`: el operador que llegó (`*`) y su precedencia;
|
|
60
|
+
- `decision`: `se_lo_lleva` si `incoming` tiene más precedencia (el `2` pasa a ser el lado izquierdo del `*`) o `se_queda`
|
|
61
|
+
si no (el `2` se queda como lado derecho del `+`).
|
|
62
|
+
|
|
63
|
+
En `(1 + 2) * 3` el paréntesis cierra la suma antes de que llegue el `*`: el evento del `*` no trae `building` y su
|
|
64
|
+
`operand` es la suma completa. En `1 < 2 y 5 < 4`, el `2` se queda con el `<` porque la `y` tiene menos precedencia. Como
|
|
65
|
+
`^` agrupa a la derecha, su lado derecho compara con una precedencia menos que la suya: en `2 ^ 3 ^ 2` el segundo `^` se
|
|
66
|
+
lleva al `3`. Las llamadas, los índices y el `.` también tienen precedencia (la más alta) y producen su evento.
|
|
67
|
+
|
|
68
|
+
**Errores.** Los nodos que estaban abiertos cuando falló la sentencia se abandonan: no tienen `exit` y no están en el AST.
|
|
69
|
+
`recover` los lista (`abandoned`, del más interno al más externo) junto con los tokens que saltó (`skipped.from` hasta
|
|
70
|
+
`skipped.to - 1`); el siguiente `consume` es el token `skipped.to`. Los errores léxicos no aparecen en la traza; el lexer
|
|
71
|
+
ya los reporta en `errors`.
|
|
72
|
+
|
|
73
|
+
La traza no cambia nada del análisis: sin `trace`, el parser no la construye; con ella, el AST y sus `id` son los mismos; el
|
|
74
|
+
mismo código produce siempre la misma traza, y los eventos se pueden mandar desde un Web Worker (en el sandbox, con la
|
|
75
|
+
opción `parseTrace` de `cargar`).
|
|
76
|
+
|
|
32
77
|
## 3. Compilador (`src/runtime/compiler.ts`)
|
|
33
78
|
|
|
34
79
|
`compile(program)` traduce el AST a una lista de instrucciones para una máquina de pila. Por ejemplo, `x += 1` se convierte en:
|
|
@@ -171,4 +216,4 @@ cualquier otra vista.
|
|
|
171
216
|
|
|
172
217
|
1. `bun run repl` y escribe programas pequeños.
|
|
173
218
|
2. Lee los tests de `tests/` en el orden del pipeline: `lexer`, `parser`, `machine`, `stepping`.
|
|
174
|
-
3. Usa `tokenize`, `
|
|
219
|
+
3. Usa `tokenize`, `traceParse` y `run(código, { trace: true })` para ver cada etapa del mismo programa.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Codexivo v1 — Especificación del lenguaje
|
|
2
2
|
|
|
3
|
-
> **Estado:**
|
|
4
|
-
>
|
|
3
|
+
> **Estado:** implementada en la versión 1.0.0 (§16). Lo que funciona está en [`lenguaje.md`](./lenguaje.md), y todos sus
|
|
4
|
+
> ejemplos se prueban automáticamente.
|
|
5
5
|
>
|
|
6
6
|
> Las secciones marcadas con **✅ Decidido** fueron decisiones abiertas que ya se resolvieron.
|
|
7
7
|
|
|
@@ -429,24 +429,26 @@ calculadora.suma(1, 3); // 4
|
|
|
429
429
|
|
|
430
430
|
## 7. Errores
|
|
431
431
|
|
|
432
|
-
Cada error es un objeto, no solo un texto
|
|
432
|
+
Cada error es un objeto, no solo un texto. Los campos de la API van en inglés, como el resto (`kind`, `span`); todo lo que
|
|
433
|
+
lee quien aprende va en español:
|
|
433
434
|
|
|
434
435
|
```ts
|
|
435
436
|
{
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
pila: ["programa", "promedio(lista)"]
|
|
437
|
+
kind: "léxico" | "sintaxis" | "ejecución" | "límite",
|
|
438
|
+
code: "DIVISIÓN_ENTRE_CERO",
|
|
439
|
+
message: "No se puede dividir entre cero.",
|
|
440
|
+
hint: "Revisa que `divisor` no valga 0 antes de dividir.",
|
|
441
|
+
span: { start: { line: 4, column: 12, offset: 80 }, end: { line: 4, column: 23, offset: 91 } },
|
|
442
|
+
callStack: ["programa", "promedio"]
|
|
443
443
|
}
|
|
444
444
|
```
|
|
445
445
|
|
|
446
|
+
Cada `code` tiene una explicación larga en el catálogo (`catalog.errors`).
|
|
447
|
+
|
|
446
448
|
- Los mensajes dicen **qué pasó** y la pista **qué revisar**, en español correcto y con acentos.
|
|
447
449
|
- El parser se recupera y reporta varios errores, pero el sandbox destaca el primero.
|
|
448
|
-
- `afirmar(condición, mensaje)` detiene el programa si la condición es falsa
|
|
449
|
-
directa para practicar verificación.
|
|
450
|
+
- `afirmar(condición, mensaje)` detiene el programa con `AFIRMACIÓN_FALLIDA` si la condición es falsa; el mensaje
|
|
451
|
+
es opcional. Es la herramienta más directa para practicar verificación.
|
|
450
452
|
- Sin `intentar`/`capturar` en v1: un error detiene el programa, y eso es lo que queremos que se vea.
|
|
451
453
|
|
|
452
454
|
## 8. Biblioteca estándar
|
|
@@ -655,13 +657,16 @@ estáticos y `según`/`caso`. Se pueden reconsiderar después, siempre con el pr
|
|
|
655
657
|
|
|
656
658
|
Codexivo no tiene usuarios todavía, así que v1 se permite romper compatibilidad:
|
|
657
659
|
|
|
658
|
-
- condiciones estrictas (
|
|
660
|
+
- condiciones estrictas (§4.5);
|
|
659
661
|
- error al llamar con un número distinto de argumentos;
|
|
660
662
|
- error al redeclarar o al asignar sin declarar;
|
|
661
663
|
- `agregar` pasa a modificar la lista (antes regresaba una nueva);
|
|
662
664
|
- un procedimiento sin `regresa` regresa `nulo` (antes regresaba el valor de su última expresión);
|
|
663
665
|
- `1 / 0` pasa a ser error;
|
|
664
|
-
- `ARREGLO` pasa a llamarse `lista` en mensajes y en `tipo()
|
|
666
|
+
- `ARREGLO` pasa a llamarse `lista` en mensajes y en `tipo()`;
|
|
667
|
+
- palabras reservadas nuevas (§2.5): `constante`, `dominio`, `repetir`, `veces`, `desde`, `hasta`, `con_paso`,
|
|
668
|
+
`para_cada`, `en`, `romper`, `continuar`, `mod`, `div` y las comparaciones en palabras ya no sirven como nombres;
|
|
669
|
+
- sin `;`, el salto de línea termina la expresión (§2.4): una expresión partida antes de un operador es un error.
|
|
665
670
|
|
|
666
671
|
## 15. Decisiones
|
|
667
672
|
|
package/docs/integracion.md
CHANGED
|
@@ -62,6 +62,7 @@ promesa se rechaza.
|
|
|
62
62
|
| `world` | — | Un mundo del [modo mundo](./modo-mundo.md), en JSON |
|
|
63
63
|
| `detail` | `decisiones` | Eventos por tanda: `nada`, `pasos` (`paso` y `robot`), `decisiones` (más `decide`) o `todo` |
|
|
64
64
|
| `batchSize` | 50 000 | Operaciones por tanda |
|
|
65
|
+
| `parseTrace` | `false` | `cargado` trae `parseTrace`: la [traza del parser](./arquitectura.md#traza-del-parser) |
|
|
65
66
|
|
|
66
67
|
## Tandas
|
|
67
68
|
|
package/docs/lenguaje.md
CHANGED
|
@@ -381,14 +381,15 @@ Todo está disponible sin importar nada. Los nombres con acento también se acep
|
|
|
381
381
|
|
|
382
382
|
### Entrada, salida y tipos
|
|
383
383
|
|
|
384
|
-
| Procedimiento
|
|
385
|
-
|
|
|
386
|
-
| `imprimir(a, b, …)`
|
|
387
|
-
| `leer()`
|
|
388
|
-
| `leer_número()`
|
|
389
|
-
| `tipo(x)`
|
|
390
|
-
| `texto(x)`, `número(x)`
|
|
391
|
-
| `longitud(x)`
|
|
384
|
+
| Procedimiento | Qué hace |
|
|
385
|
+
| ------------------------------ | -------------------------------------------------------------------------------------------------------- |
|
|
386
|
+
| `imprimir(a, b, …)` | Escribe los valores separados por un espacio y termina la línea |
|
|
387
|
+
| `leer()` | Siguiente línea de entrada como texto, o `nulo` si ya no hay |
|
|
388
|
+
| `leer_número()` | Siguiente línea de entrada como número |
|
|
389
|
+
| `tipo(x)` | `"número"`, `"texto"`, `"lógico"`, `"nulo"`, `"lista"`, `"diccionario"`, `"dominio"` o `"procedimiento"` |
|
|
390
|
+
| `texto(x)`, `número(x)` | Conversiones explícitas |
|
|
391
|
+
| `longitud(x)` | Caracteres de un texto, elementos de una lista o claves de un diccionario |
|
|
392
|
+
| `afirmar(condición, mensaje?)` | Detiene el programa si la condición es falsa (ver [Errores](#errores)) |
|
|
392
393
|
|
|
393
394
|
### Matemáticas
|
|
394
395
|
|
|
@@ -493,6 +494,19 @@ Error de ejecución en la línea 2, columna 5: No se puede dividir entre cero.
|
|
|
493
494
|
Pista: Revisa que el divisor no valga 0 antes de dividir.
|
|
494
495
|
```
|
|
495
496
|
|
|
497
|
+
Con `afirmar` se escribe lo que se espera que sea cierto. Si no lo es, el programa se detiene en esa línea, antes de que el
|
|
498
|
+
error llegue más lejos:
|
|
499
|
+
|
|
500
|
+
```codexivo
|
|
501
|
+
variable precio = 120
|
|
502
|
+
variable descuento = 20
|
|
503
|
+
variable total = precio - descuento
|
|
504
|
+
afirmar(total es_menor_que precio, "el descuento tenía que bajar el precio")
|
|
505
|
+
imprimir("Total:", total)
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Un error detiene el programa: no hay forma de atraparlo y seguir, porque lo que se busca es verlo.
|
|
509
|
+
|
|
496
510
|
## Puente hacia otros lenguajes
|
|
497
511
|
|
|
498
512
|
Muchos lenguajes (C, Java, JavaScript) cuentan con un `for` de tres partes. Codexivo también lo tiene, para practicar antes de
|
package/package.json
CHANGED
package/src/catalog.ts
CHANGED
package/src/error-codes.ts
CHANGED
|
@@ -186,6 +186,11 @@ export const ERROR_CODES: Readonly<Record<string, { kind: ErrorKind; explanation
|
|
|
186
186
|
},
|
|
187
187
|
|
|
188
188
|
// ejecución
|
|
189
|
+
AFIRMACIÓN_FALLIDA: {
|
|
190
|
+
kind: 'ejecución',
|
|
191
|
+
explanation:
|
|
192
|
+
'Una llamada a `afirmar(condición, mensaje)` encontró que su condición era falsa. Afirmar es escribir lo que esperas que sea cierto en ese punto del programa; si no lo es, el programa se detiene ahí, antes de que el error llegue más lejos. Revisa los valores de las variables justo antes de esa línea.',
|
|
193
|
+
},
|
|
189
194
|
CANTIDAD_DE_ARGUMENTOS: {
|
|
190
195
|
kind: 'ejecución',
|
|
191
196
|
explanation:
|
package/src/index.ts
CHANGED
|
@@ -6,8 +6,8 @@ export type { ErrorInfo, ErrorKind } from './errors';
|
|
|
6
6
|
|
|
7
7
|
export { tokenize } from './syntax/lexer';
|
|
8
8
|
export type { LexResult } from './syntax/lexer';
|
|
9
|
-
export { parse } from './syntax/parser';
|
|
10
|
-
export type { ParseResult } from './syntax/parser';
|
|
9
|
+
export { parse, traceParse } from './syntax/parser';
|
|
10
|
+
export type { ParseResult, ParseOptions, ParseEvent, PrecedenceOperator } from './syntax/parser';
|
|
11
11
|
export { TokenType, KEYWORDS, isKeyword } from './syntax/token';
|
|
12
12
|
export type { Token, Span, Position } from './syntax/token';
|
|
13
13
|
export { childrenOf, walk } from './syntax/ast';
|
package/src/runtime/builtins.ts
CHANGED
|
@@ -358,6 +358,50 @@ const library: NativeValue[] = [
|
|
|
358
358
|
},
|
|
359
359
|
),
|
|
360
360
|
|
|
361
|
+
// verificación
|
|
362
|
+
define(
|
|
363
|
+
'afirmar',
|
|
364
|
+
[1, 2],
|
|
365
|
+
{
|
|
366
|
+
level: 'principiante',
|
|
367
|
+
category: 'verificación',
|
|
368
|
+
params: [
|
|
369
|
+
{ name: 'condición', type: 'lógico' },
|
|
370
|
+
{ name: 'mensaje', type: 'texto', optional: true },
|
|
371
|
+
],
|
|
372
|
+
returns: 'nulo',
|
|
373
|
+
description:
|
|
374
|
+
'Detiene el programa con un error si la condición es falsa. Sirve para escribir lo que esperas que sea cierto y comprobarlo.',
|
|
375
|
+
examples: [
|
|
376
|
+
{ code: 'variable total = 2 + 2\nafirmar(total es_igual_a 4, "la suma está mal")\ntotal', result: '4' },
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
(args, context) => {
|
|
380
|
+
const condition = args[0]!;
|
|
381
|
+
if (typeof condition !== 'boolean') {
|
|
382
|
+
throw context.fail(
|
|
383
|
+
'TIPO_DE_ARGUMENTO',
|
|
384
|
+
`\`afirmar\` espera una condición \`verdadero\` o \`falso\`, pero recibió ${describeType(typeOf(condition))} (${repr(condition)}).`,
|
|
385
|
+
'¿Quisiste comparar? Por ejemplo: `afirmar(x es_mayor_que 0)`.',
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
const message = args[1];
|
|
389
|
+
if (message !== undefined && typeof message !== 'string') {
|
|
390
|
+
throw context.fail(
|
|
391
|
+
'TIPO_DE_ARGUMENTO',
|
|
392
|
+
`El mensaje de \`afirmar\` tiene que ser un texto, pero es ${describeType(typeOf(message))}.`,
|
|
393
|
+
'Escribe el mensaje entre comillas: `afirmar(x es_mayor_que 0, "x tenía que ser positivo")`.',
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
if (condition) return null;
|
|
397
|
+
throw context.fail(
|
|
398
|
+
'AFIRMACIÓN_FALLIDA',
|
|
399
|
+
message ? `La afirmación no se cumplió: ${message}` : 'La afirmación no se cumplió.',
|
|
400
|
+
'Lo que afirmaste que era cierto no lo es en este punto. Revisa los valores de las variables justo antes de esta línea.',
|
|
401
|
+
);
|
|
402
|
+
},
|
|
403
|
+
),
|
|
404
|
+
|
|
361
405
|
// matemáticas
|
|
362
406
|
define(
|
|
363
407
|
'absoluto',
|
package/src/runtime/values.ts
CHANGED
package/src/sandbox-host.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Machine, serialize } from './runtime/machine';
|
|
|
5
5
|
import type { MachineEvent, MachineOptions, MachineStatus, SerializedValue, Snapshot } from './runtime/machine';
|
|
6
6
|
import { compile } from './runtime/compiler';
|
|
7
7
|
import { parse } from './syntax/parser';
|
|
8
|
+
import type { ParseEvent } from './syntax/parser';
|
|
8
9
|
import type * as ast from './syntax/ast';
|
|
9
10
|
import type { Token } from './syntax/token';
|
|
10
11
|
import { createWorld } from './world';
|
|
@@ -39,6 +40,8 @@ export interface SandboxOptions {
|
|
|
39
40
|
detail?: EventDetail;
|
|
40
41
|
/** Operaciones por tanda antes de ceder el control. Por defecto 50 000. */
|
|
41
42
|
batchSize?: number;
|
|
43
|
+
/** Si `cargado` trae `parseTrace`: cómo construyó el parser el árbol, paso a paso. Por defecto `false`. */
|
|
44
|
+
parseTrace?: boolean;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
export type SandboxRequest =
|
|
@@ -67,6 +70,8 @@ export interface Loaded {
|
|
|
67
70
|
errors: ErrorInfo[];
|
|
68
71
|
/** Semilla de `aleatorio` de esta carga. */
|
|
69
72
|
seed: number;
|
|
73
|
+
/** La traza del parser (ver `parse(código, { trace: true })`), solo con la opción `parseTrace`. */
|
|
74
|
+
parseTrace?: ParseEvent[];
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
export interface Progress {
|
|
@@ -193,10 +198,18 @@ export class SandboxHost {
|
|
|
193
198
|
this.inputClosed = false;
|
|
194
199
|
this.events = [];
|
|
195
200
|
this.sentOutput = 0;
|
|
196
|
-
const { program, tokens, errors } = parse(source);
|
|
201
|
+
const { program, tokens, errors, trace } = parse(source, { trace: options.parseTrace ?? false });
|
|
197
202
|
this.program = errors.length === 0 ? program : undefined;
|
|
198
203
|
this.machine = this.program ? this.createMachine() : undefined;
|
|
199
|
-
this.post({
|
|
204
|
+
this.post({
|
|
205
|
+
type: 'cargado',
|
|
206
|
+
id,
|
|
207
|
+
tokens,
|
|
208
|
+
program,
|
|
209
|
+
errors: errors.map(error => error.toJSON()),
|
|
210
|
+
seed: this.seed,
|
|
211
|
+
...(trace ? { parseTrace: trace } : {}),
|
|
212
|
+
});
|
|
200
213
|
}
|
|
201
214
|
|
|
202
215
|
private createMachine(): Machine {
|