codexivo 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ import { run } from './engine';
2
+ import type { ErrorInfo } from './errors';
3
+ import { equals, repr } from './runtime/values';
4
+ import type { DictKey, Value } from './runtime/values';
5
+
6
+ /**
7
+ * Retos: un conjunto de casos que se ejecutan con su propia entrada y su propio límite de operaciones. Corre
8
+ * igual en el worker del sitio y en Bun, así los retos se prueban en CI.
9
+ *
10
+ * - Estilo `salida` (como HackerRank): el programa lee con `leer()` y se compara lo que imprime.
11
+ * - Estilo `regresa` (como LeetCode): se llama a `resolver(…)` (u otro procedimiento) con argumentos y se compara
12
+ * lo que regresa, por valor.
13
+ */
14
+
15
+ /** Un valor de JSON: los arreglos se vuelven listas y los objetos, diccionarios. */
16
+ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
17
+
18
+ export interface ChallengeCase {
19
+ name?: string;
20
+ /** Entrada estándar: un texto (una línea por renglón) o una lista de líneas. */
21
+ input?: string | string[];
22
+ /** Estilo `salida`: lo que el programa tiene que imprimir. */
23
+ expectedOutput?: string;
24
+ /** Estilo `regresa`: los argumentos para el procedimiento. */
25
+ args?: JsonValue[];
26
+ /** Estilo `regresa`: lo que tiene que regresar. */
27
+ expected?: JsonValue;
28
+ /** Límite de operaciones de este caso; si no, el del reto. */
29
+ maxOperations?: number;
30
+ }
31
+
32
+ export interface Challenge {
33
+ style: 'salida' | 'regresa';
34
+ cases: ChallengeCase[];
35
+ /** Estilo `regresa`: el procedimiento que se llama. Por defecto `resolver`. */
36
+ procedure?: string;
37
+ /** Límite de operaciones por caso. Por defecto 1 000 000. */
38
+ maxOperations?: number;
39
+ /**
40
+ * Con `true`, la salida tiene que ser idéntica. Si no, se ignoran los espacios al final de cada línea y las
41
+ * líneas vacías del final.
42
+ */
43
+ exactOutput?: boolean;
44
+ }
45
+
46
+ export interface CaseResult {
47
+ name: string;
48
+ passed: boolean;
49
+ output: string;
50
+ expectedOutput?: string;
51
+ /** Estilo `regresa`: lo que regresó y lo que se esperaba, como se escribirían en código. */
52
+ result?: string;
53
+ expected?: string;
54
+ operations: number;
55
+ error?: ErrorInfo;
56
+ }
57
+
58
+ export interface ChallengeResult {
59
+ passed: boolean;
60
+ cases: CaseResult[];
61
+ }
62
+
63
+ const DEFAULT_MAX_OPERATIONS = 1_000_000;
64
+
65
+ /** Un valor de JSON escrito como literal de Codexivo. */
66
+ const literal = (value: JsonValue): string => {
67
+ if (value === null) return 'nulo';
68
+ if (typeof value === 'boolean') return value ? 'verdadero' : 'falso';
69
+ if (typeof value === 'number') {
70
+ if (!Number.isFinite(value)) throw new Error(`El reto tiene un número que no es finito: ${value}.`);
71
+ return value < 0 ? `(${value})` : String(value);
72
+ }
73
+ if (typeof value === 'string') return JSON.stringify(value);
74
+ if (Array.isArray(value)) return `[${value.map(literal).join(', ')}]`;
75
+ return `{ ${Object.entries(value)
76
+ .map(([key, item]) => `${JSON.stringify(key)}: ${literal(item)}`)
77
+ .join(', ')} }`;
78
+ };
79
+
80
+ /** Un valor de JSON como valor de Codexivo, para comparar por valor con `equals`. */
81
+ const toValue = (value: JsonValue): Value => {
82
+ if (value === null || typeof value !== 'object') return value;
83
+ if (Array.isArray(value)) return { kind: 'lista', id: 0, items: value.map(toValue) };
84
+ return {
85
+ kind: 'diccionario',
86
+ id: 0,
87
+ entries: new Map<DictKey, Value>(Object.entries(value).map(([key, item]) => [key, toValue(item)])),
88
+ };
89
+ };
90
+
91
+ const normalize = (text: string): string =>
92
+ text
93
+ .replace(/\r\n/g, '\n')
94
+ .split('\n')
95
+ .map(line => line.replace(/[ \t]+$/, ''))
96
+ .join('\n')
97
+ .replace(/\n+$/, '');
98
+
99
+ /** Ejecuta cada caso del reto con el código de quien lo resuelve. */
100
+ export const evaluarReto = (source: string, challenge: Challenge): ChallengeResult => {
101
+ const procedure = challenge.procedure ?? 'resolver';
102
+ const cases = challenge.cases.map((testCase, index): CaseResult => {
103
+ const name = testCase.name ?? `Caso ${index + 1}`;
104
+ const maxOperations = testCase.maxOperations ?? challenge.maxOperations ?? DEFAULT_MAX_OPERATIONS;
105
+ const input = testCase.input ?? [];
106
+
107
+ if (challenge.style === 'salida') {
108
+ const result = run(source, { input, maxOperations });
109
+ const expectedOutput = testCase.expectedOutput ?? '';
110
+ const same = challenge.exactOutput
111
+ ? result.output === expectedOutput
112
+ : normalize(result.output) === normalize(expectedOutput);
113
+ return {
114
+ name,
115
+ passed: result.ok && same,
116
+ output: result.output,
117
+ expectedOutput,
118
+ operations: result.operations,
119
+ ...(result.errors[0] ? { error: result.errors[0] } : {}),
120
+ };
121
+ }
122
+
123
+ // La llamada va al final, en su propia línea, para que los errores del programa conserven su lugar.
124
+ const call = `\n;\nregresa ${procedure}(${(testCase.args ?? []).map(literal).join(', ')});\n`;
125
+ const result = run(source + call, { input, maxOperations });
126
+ const expected = toValue(testCase.expected ?? null);
127
+ return {
128
+ name,
129
+ passed: result.ok && equals(result.result, expected),
130
+ output: result.output,
131
+ result: result.resultText,
132
+ expected: repr(expected),
133
+ operations: result.operations,
134
+ ...(result.errors[0] ? { error: result.errors[0] } : {}),
135
+ };
136
+ });
137
+ return { passed: cases.every(testCase => testCase.passed), cases };
138
+ };
package/src/engine.ts CHANGED
@@ -7,6 +7,7 @@ import { Machine } from './runtime/machine';
7
7
  import type { MachineEvent, MachineOptions, MachineStatus } from './runtime/machine';
8
8
  import { repr, Scope } from './runtime/values';
9
9
  import type { Value } from './runtime/values';
10
+ import type { WorldState } from './world';
10
11
 
11
12
  export interface PreparedProgram {
12
13
  program: ast.Program;
@@ -41,6 +42,10 @@ export interface RunResult {
41
42
  output: string;
42
43
  errors: ErrorInfo[];
43
44
  operations: number;
45
+ /** Semilla de `aleatorio` que usó la ejecución; pásala en `seed` para repetirla. */
46
+ seed?: number;
47
+ /** Estado final del mundo, si se ejecutó en modo mundo. */
48
+ world?: WorldState;
44
49
  events?: MachineEvent[];
45
50
  /** `true` si se guardaron menos eventos de los que ocurrieron. */
46
51
  eventsTruncated?: boolean;
@@ -81,6 +86,8 @@ export const run = (source: string, options: RunOptions = {}): RunResult => {
81
86
  output: machine.output,
82
87
  errors: machine.error ? [machine.error.toJSON()] : [],
83
88
  operations: machine.operations,
89
+ seed: machine.seed,
90
+ ...(options.world ? { world: options.world.state() } : {}),
84
91
  ...(trace ? { events, eventsTruncated } : {}),
85
92
  };
86
93
  };
@@ -0,0 +1,432 @@
1
+ import type { ErrorKind } from './errors';
2
+
3
+ /**
4
+ * Todos los códigos de error, con una explicación larga para la página de «errores comunes» del sitio. Una
5
+ * prueba revisa que cada código que se usa en `src/` esté aquí, y que aquí no sobre ninguno.
6
+ */
7
+ export const ERROR_CODES: Readonly<Record<string, { kind: ErrorKind; explanation: string }>> = {
8
+ // léxico
9
+ CARÁCTER_DESCONOCIDO: {
10
+ kind: 'léxico',
11
+ explanation:
12
+ 'El código tiene un carácter que Codexivo no usa, como `&`, `|` o `@`. Suele pasar al copiar código de otro lenguaje o de un documento: las comillas tipográficas (“ ”) no son comillas de código. Para «y» y «o» se escriben las palabras `y` y `o`.',
13
+ },
14
+ COMENTARIO_SIN_CERRAR: {
15
+ kind: 'léxico',
16
+ explanation:
17
+ 'Un comentario `/*` sigue hasta encontrar `*/`. Si falta el cierre, todo lo que viene después queda dentro del comentario y no se ejecuta.',
18
+ },
19
+ ESCAPE_DESCONOCIDO: {
20
+ kind: 'léxico',
21
+ explanation:
22
+ 'Dentro de un texto, `\\` empieza una secuencia de escape. Las válidas son `\\n` (salto de línea), `\\t` (tabulador), `\\\\`, `\\"` y `\\\'`. Para escribir una diagonal invertida se escribe dos veces.',
23
+ },
24
+ NOMBRE_EMPIEZA_CON_NÚMERO: {
25
+ kind: 'léxico',
26
+ explanation:
27
+ 'Los nombres pueden llevar dígitos, pero no al principio: `x2` es un nombre y `2x` no. Si querías multiplicar, escribe el operador: `2 * x`.',
28
+ },
29
+ SEPARADOR_INVÁLIDO: {
30
+ kind: 'léxico',
31
+ explanation:
32
+ 'En los números, `_` solo sirve para separar grupos de dígitos y que se lean mejor: `1_000_000`. Tiene que ir entre dos dígitos; `1__0` o `1_` no son números.',
33
+ },
34
+ TEXTO_SIN_CERRAR: {
35
+ kind: 'léxico',
36
+ explanation:
37
+ 'Un texto empieza y termina con la misma comilla, en la misma línea. Si falta la comilla final, Codexivo no sabe dónde termina. Para un salto de línea dentro del texto usa `\\n`.',
38
+ },
39
+
40
+ // sintaxis
41
+ ANIDAMIENTO_EXCESIVO: {
42
+ kind: 'sintaxis',
43
+ explanation:
44
+ 'La expresión tiene demasiados niveles de paréntesis o de operaciones anidadas. Guarda partes en variables intermedias: además de evitar el error, el programa se lee mejor.',
45
+ },
46
+ ASIGNACIÓN_INVÁLIDA: {
47
+ kind: 'sintaxis',
48
+ explanation:
49
+ 'A la izquierda de `=` (o de `+=`, `++`…) tiene que ir algo que pueda guardar un valor: una variable (`x = 1`), un elemento (`lista[0] = 1`) o un campo (`persona.edad = 16`). `5 = x` o `f(x) = 1` no tienen dónde guardarlo.',
50
+ },
51
+ BLOQUE_SIN_CERRAR: {
52
+ kind: 'sintaxis',
53
+ explanation:
54
+ 'Cada `{` necesita su `}`. El error señala la llave que abrió el bloque; revisa que cada `si`, `mientras` o procedimiento cierre su bloque antes de que empiece lo que sigue.',
55
+ },
56
+ CAMPO_ESPERADO: {
57
+ kind: 'sintaxis',
58
+ explanation:
59
+ 'Después de `.` va el nombre de un campo de un diccionario: `persona.edad`. Solo funciona con claves que son nombres válidos y que no son palabras reservadas; para las demás se escribe `d["clave"]`.',
60
+ },
61
+ CLAVE_REPETIDA: {
62
+ kind: 'sintaxis',
63
+ explanation:
64
+ 'Un diccionario escrito entre llaves tiene la misma clave dos veces. Solo se quedaría el último valor, así que casi siempre es un error de copiar y pegar.',
65
+ },
66
+ CLAVE_SIN_COMILLAS: {
67
+ kind: 'sintaxis',
68
+ explanation:
69
+ 'Las claves de un diccionario son textos o números, y los textos van entre comillas: `{ "nombre": "Ana" }`. Si querías usar el valor de una variable como clave, escríbela entre paréntesis: `{ (clave): 1 }`.',
70
+ },
71
+ COMPARACIÓN_ENCADENADA: {
72
+ kind: 'sintaxis',
73
+ explanation:
74
+ '`1 < x < 5` parece natural, pero esconde dos comparaciones. En Codexivo se escriben las dos y se unen con `y`: `1 < x y x < 5`.',
75
+ },
76
+ CONTINUAR_FUERA_DE_BUCLE: {
77
+ kind: 'sintaxis',
78
+ explanation:
79
+ '`continuar` salta a la siguiente vuelta del bucle más cercano, así que solo tiene sentido dentro de uno. Dentro de un procedimiento no afecta al bucle desde donde se llamó.',
80
+ },
81
+ DICCIONARIO_COMO_SENTENCIA: {
82
+ kind: 'sintaxis',
83
+ explanation:
84
+ 'Al inicio de una sentencia, `{` abre un bloque. Un diccionario tiene que estar en una expresión: guárdalo en una variable (`variable d = { "a": 1 }`) o pásalo a un procedimiento.',
85
+ },
86
+ DOMINIO_SOLO_DECLARACIONES: {
87
+ kind: 'sintaxis',
88
+ explanation:
89
+ 'Un `dominio` es un espacio de nombres: solo agrupa declaraciones (`variable`, `constante`, `procedimiento` y otros `dominio`). El código que hace algo, como un `si` o un `imprimir`, va dentro de un procedimiento del dominio o fuera de él.',
90
+ },
91
+ EN_SIN_PARA_CADA: {
92
+ kind: 'sintaxis',
93
+ explanation: '`en` es parte de `para_cada elemento en lista { … }` y no se puede usar sola ni como nombre.',
94
+ },
95
+ EXPRESIÓN_ESPERADA: {
96
+ kind: 'sintaxis',
97
+ explanation:
98
+ 'En ese lugar iba un valor (un número, un texto, una variable, una llamada…) y apareció otra cosa. Suele faltar un operando, como en `x = * 2`, o se usó una palabra reservada como si fuera un nombre.',
99
+ },
100
+ EXPRESIÓN_INCOMPLETA: {
101
+ kind: 'sintaxis',
102
+ explanation:
103
+ 'El programa terminó a mitad de una expresión, como en `x = 1 +`. Revisa que no falte el último valor.',
104
+ },
105
+ FIN_DE_SENTENCIA: {
106
+ kind: 'sintaxis',
107
+ explanation:
108
+ 'Dos sentencias en la misma línea tienen que separarse con `;`. Si querías combinar dos valores, falta un operador entre ellos: `1 2` no significa nada, pero `1 + 2` sí.',
109
+ },
110
+ HASTA_QUE_SIN_HACER: {
111
+ kind: 'sintaxis',
112
+ explanation:
113
+ '`hasta_que (condición)` cierra un bucle `hacer { … }` y tiene que ir justo después de su `}`. Para repetir mientras algo sea verdadero, usa `mientras`.',
114
+ },
115
+ INCREMENTO_EN_EXPRESIÓN: {
116
+ kind: 'sintaxis',
117
+ explanation:
118
+ '`i++` e `i--` son sentencias: cambian la variable y nada más. No se pueden usar dentro de una expresión (`x = i++`) ni antes del nombre (`++i`). Escribe `i++` en su propia línea y después usa `i`.',
119
+ },
120
+ LLAVE_SOBRANTE: {
121
+ kind: 'sintaxis',
122
+ explanation: 'Hay una `}` que no cierra ningún bloque. Suele ser una llave de más, o una que falta más arriba.',
123
+ },
124
+ NOMBRE_ESPERADO: {
125
+ kind: 'sintaxis',
126
+ explanation:
127
+ 'Después de `variable`, `constante`, `procedimiento` o `para_cada` va un nombre: letras, dígitos y `_`, sin empezar con dígito.',
128
+ },
129
+ NOMBRE_REPETIDO: {
130
+ kind: 'sintaxis',
131
+ explanation:
132
+ 'Un procedimiento con nombre existe desde el inicio de su bloque, para poder llamarlo antes de la línea donde se escribe. Por eso su nombre no se puede repetir en el mismo bloque, ni con otro procedimiento ni con una variable.',
133
+ },
134
+ PALABRA_DESCONOCIDA: {
135
+ kind: 'sintaxis',
136
+ explanation:
137
+ 'Se parece a una palabra de Codexivo, pero no lo es. Las palabras compuestas llevan guion bajo: `si_no`, `pero_si`, `hasta_que`.',
138
+ },
139
+ PALABRA_RESERVADA: {
140
+ kind: 'sintaxis',
141
+ explanation:
142
+ 'Las palabras reservadas (`si`, `mientras`, `y`, `veces`, `mod`…) tienen un significado fijo y no se pueden usar como nombres. Elige otro nombre, por ejemplo agregando una palabra: `valor_inicial`.',
143
+ },
144
+ PARÁMETRO_REPETIDO: {
145
+ kind: 'sintaxis',
146
+ explanation:
147
+ 'Dos parámetros de un procedimiento (o las dos variables de un `para_cada`) tienen el mismo nombre, así que no habría forma de distinguirlos.',
148
+ },
149
+ PROCEDIMIENTO_CON_NOMBRE: {
150
+ kind: 'sintaxis',
151
+ explanation:
152
+ '`procedimiento nombre(…) { … }` es una sentencia y no puede ir dentro de una expresión. Como valor, el procedimiento se escribe sin nombre: `variable doble = procedimiento(x) { regresa x * 2 }`.',
153
+ },
154
+ RAMA_SIN_SI: {
155
+ kind: 'sintaxis',
156
+ explanation:
157
+ '`pero_si` y `si_no` continúan un `si` y van justo después de la `}` de su bloque. Una sentencia en medio rompe la cadena.',
158
+ },
159
+ REPETIR_SIN_CANTIDAD: {
160
+ kind: 'sintaxis',
161
+ explanation: 'Falta cuántas veces se repite el bloque. La forma completa es `repetir 4 veces { … }`.',
162
+ },
163
+ ROMPER_FUERA_DE_BUCLE: {
164
+ kind: 'sintaxis',
165
+ explanation:
166
+ '`romper` sale del bucle más cercano, así que solo tiene sentido dentro de uno. Para salir de un procedimiento se usa `regresa`.',
167
+ },
168
+ SENTENCIA_EN_EXPRESIÓN: {
169
+ kind: 'sintaxis',
170
+ explanation:
171
+ 'Palabras como `si`, `mientras` o `variable` empiezan una sentencia y no dan un valor, así que no pueden ir donde se espera uno, como a la derecha de `=`.',
172
+ },
173
+ SI_NO_SI: {
174
+ kind: 'sintaxis',
175
+ explanation:
176
+ 'Después de `si_no` va directamente un bloque. Para probar otra condición se usa `pero_si (condición) { … }`.',
177
+ },
178
+ TOKEN_ESPERADO: {
179
+ kind: 'sintaxis',
180
+ explanation:
181
+ 'Falta un símbolo o una palabra que la sentencia necesita, como el `(` de una condición o el `veces` de `repetir`. El mensaje dice qué se esperaba y qué se encontró.',
182
+ },
183
+ ÍNDICE_VACÍO: {
184
+ kind: 'sintaxis',
185
+ explanation: 'Entre los corchetes va la posición o la clave: `lista[0]`, `d["nombre"]`.',
186
+ },
187
+
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
+ },
194
+ CANTIDAD_DE_ARGUMENTOS: {
195
+ kind: 'ejecución',
196
+ explanation:
197
+ 'Un procedimiento se llama con exactamente tantos valores como parámetros tiene. Si faltan o sobran, es un error: Codexivo no rellena los que faltan.',
198
+ },
199
+ CHOQUE_CON_PARED: {
200
+ kind: 'ejecución',
201
+ explanation:
202
+ 'El robot intentó avanzar hacia una pared o fuera del mundo. Antes de `avanzar()`, pregunta con `frente_libre()`.',
203
+ },
204
+ CLAVE_INEXISTENTE: {
205
+ kind: 'ejecución',
206
+ explanation:
207
+ 'Se leyó una clave que el diccionario no tiene. Pregunta antes con `tiene(d, clave)`, o revisa que la clave esté bien escrita: `"Edad"` y `"edad"` son distintas, igual que `1` y `"1"`.',
208
+ },
209
+ CLAVE_INVÁLIDA: {
210
+ kind: 'ejecución',
211
+ explanation:
212
+ 'Las claves de un diccionario son textos o números. Una lista, un lógico o `nulo` no pueden ser clave.',
213
+ },
214
+ COLA_VACÍA: {
215
+ kind: 'ejecución',
216
+ explanation:
217
+ 'Se intentó sacar o consultar el frente de una cola que no tiene nada. Revisa antes con `longitud(cola) > 0`.',
218
+ },
219
+ COMPARACIÓN_INVÁLIDA: {
220
+ kind: 'ejecución',
221
+ explanation:
222
+ '`<`, `>`, `<=` y `>=` comparan dos números o dos textos. Comparar un número con un texto no tiene un orden claro; convierte uno de los dos con `número(…)` o `texto(…)`.',
223
+ },
224
+ CONDICIÓN_NO_LÓGICA: {
225
+ kind: 'ejecución',
226
+ explanation:
227
+ 'Las condiciones de `si`, `mientras`, `hasta_que` y `para` tienen que ser `verdadero` o `falso`. En otros lenguajes `si (x)` funciona con números; en Codexivo se dice lo que se quiere preguntar: `si (x es_distinto_de 0)`.',
228
+ },
229
+ CONSTANTE_REASIGNADA: {
230
+ kind: 'ejecución',
231
+ explanation:
232
+ 'Una `constante` no cambia de valor. Si necesitas cambiarlo, declárala con `variable` en lugar de `constante`.',
233
+ },
234
+ DICCIONARIO_MODIFICADO_AL_RECORRER: {
235
+ kind: 'ejecución',
236
+ explanation:
237
+ 'Se agregaron o quitaron claves de un diccionario mientras `para_cada` lo recorría. Recorre `claves(d)`, que es una lista nueva, o junta los cambios y aplícalos al terminar.',
238
+ },
239
+ DIVISIÓN_ENTRE_CERO: {
240
+ kind: 'ejecución',
241
+ explanation:
242
+ 'Dividir entre cero (con `/`, `div`, `mod` o `%`) no tiene resultado. Revisa el divisor antes de dividir.',
243
+ },
244
+ LISTA_MODIFICADA_AL_RECORRER: {
245
+ kind: 'ejecución',
246
+ explanation:
247
+ 'La lista cambió de tamaño mientras `para_cada` la recorría, así que no queda claro qué elemento sigue. Cambiar elementos está bien; para agregar o quitar, recorre una copia o junta los cambios en otra lista.',
248
+ },
249
+ LISTA_VACÍA: {
250
+ kind: 'ejecución',
251
+ explanation:
252
+ 'El procedimiento necesita al menos un elemento y la lista está vacía. Revisa antes con `longitud(lista) > 0`.',
253
+ },
254
+ MIEMBRO_DE_SOLO_LECTURA: {
255
+ kind: 'ejecución',
256
+ explanation:
257
+ 'Desde fuera de un dominio solo se pueden cambiar sus miembros declarados con `variable`. Sus constantes, sus procedimientos y los dominios que contiene quedan fijos, para que `calculadora.suma` siempre sea el mismo procedimiento.',
258
+ },
259
+ MIEMBRO_INEXISTENTE: {
260
+ kind: 'ejecución',
261
+ explanation:
262
+ 'El dominio no tiene un miembro con ese nombre. Puede estar mal escrito; los miembros se declaran dentro de las llaves del dominio y desde fuera no se pueden agregar.',
263
+ },
264
+ MOCHILA_VACÍA: {
265
+ kind: 'ejecución',
266
+ explanation: 'El robot intentó dejar un zumbador sin tener ninguno. Pregunta antes con `tengo_zumbadores()`.',
267
+ },
268
+ NOMBRE_DESCONOCIDO: {
269
+ kind: 'ejecución',
270
+ explanation:
271
+ 'No hay ninguna variable ni procedimiento con ese nombre a la vista. Puede estar mal escrito, declararse más abajo, o haberse declarado dentro de un bloque que ya terminó.',
272
+ },
273
+ NOMBRE_RESERVADO_POR_BIBLIOTECA: {
274
+ kind: 'ejecución',
275
+ explanation:
276
+ 'Ese nombre es de la biblioteca de Codexivo y no se le puede asignar un valor. Si quieres una variable propia con ese nombre, declárala con `variable`; mientras exista, oculta al de la biblioteca.',
277
+ },
278
+ NO_ES_NÚMERO: {
279
+ kind: 'ejecución',
280
+ explanation:
281
+ 'Se esperaba un número y el texto no lo es, como `"doce"` o `"3,5"`. Los decimales se escriben con punto: `"3.5"`.',
282
+ },
283
+ NO_ES_PROCEDIMIENTO: {
284
+ kind: 'ejecución',
285
+ explanation:
286
+ 'Se intentó llamar con `( )` a algo que no es un procedimiento, como un número o una lista. Revisa que no estés usando una variable con el mismo nombre que el procedimiento.',
287
+ },
288
+ OPERANDO_NO_LÓGICO: {
289
+ kind: 'ejecución',
290
+ explanation:
291
+ '`y`, `o` y `no` unen valores `verdadero` o `falso`. Para preguntar algo sobre un número o un texto, compáralo primero: `x > 0 y nombre != ""`.',
292
+ },
293
+ PARA_LÍMITE_INVÁLIDO: {
294
+ kind: 'ejecución',
295
+ explanation:
296
+ 'En `para i desde a hasta b con_paso p`, el inicio, el final y el paso tienen que ser números. Si vienen de `leer()`, conviértelos con `número(…)` o usa `leer_número()`.',
297
+ },
298
+ PARA_PASO_CERO: {
299
+ kind: 'ejecución',
300
+ explanation:
301
+ 'Con `con_paso 0`, la variable nunca cambiaría y el `para` no llegaría al final. Usa un paso positivo para contar hacia arriba o uno negativo para contar hacia abajo.',
302
+ },
303
+ PARA_SENTIDO_CONTRARIO: {
304
+ kind: 'ejecución',
305
+ explanation:
306
+ 'El paso va en el sentido contrario al que hay que recorrer: por ejemplo, `para i desde 10 hasta 1` sin paso cuenta hacia arriba y nunca llegaría al 1. Codexivo no lo invierte solo ni hace cero vueltas en silencio: para contar hacia atrás escribe `con_paso -1`.',
307
+ },
308
+ PARA_CADA_INVÁLIDO: {
309
+ kind: 'ejecución',
310
+ explanation:
311
+ '`para_cada` recorre listas, textos y diccionarios. Para contar se recorre un rango (`para_cada i en rango(1, 10)`), y la forma con dos variables (`para_cada clave, valor en d`) es solo para diccionarios.',
312
+ },
313
+ PILA_VACÍA: {
314
+ kind: 'ejecución',
315
+ explanation:
316
+ 'Se intentó sacar o consultar el tope de una pila que no tiene nada. Revisa antes con `longitud(pila) > 0`.',
317
+ },
318
+ POTENCIA_INVÁLIDA: {
319
+ kind: 'ejecución',
320
+ explanation:
321
+ 'Un número negativo elevado a un exponente con decimales, como `(-8) ^ 0.5`, no tiene un resultado real. Con exponentes enteros sí funciona.',
322
+ },
323
+ RANGO_DEMASIADO_GRANDE: {
324
+ kind: 'ejecución',
325
+ explanation:
326
+ 'El `rango` tendría millones de números, lo que casi siempre es un error en el inicio, el final o el paso. Revísalos.',
327
+ },
328
+ RANGO_INVÁLIDO: {
329
+ kind: 'ejecución',
330
+ explanation:
331
+ 'En `subtexto` y `rebanar`, el inicio no puede estar después del fin. El fin no se incluye: `subtexto("hola", 1, 3)` es `"ol"`.',
332
+ },
333
+ RANGO_PASO_CERO: {
334
+ kind: 'ejecución',
335
+ explanation:
336
+ 'Con paso 0, `rango` nunca llegaría al final. Usa un paso positivo para contar hacia arriba o uno negativo para contar hacia abajo.',
337
+ },
338
+ RAÍZ_DE_NEGATIVO: {
339
+ kind: 'ejecución',
340
+ explanation: 'La raíz cuadrada solo existe para números de 0 en adelante.',
341
+ },
342
+ REPETICIONES_INVÁLIDAS: {
343
+ kind: 'ejecución',
344
+ explanation:
345
+ '`repetir n veces` necesita que `n` sea un número entero, 0 o más. Con un texto, conviértelo con `número(…)`; con decimales, redondéalo.',
346
+ },
347
+ ROBOT_APAGADO: {
348
+ kind: 'ejecución',
349
+ explanation: 'Después de `apagar()` el robot ya no hace ninguna acción. `apagar()` va al final del programa.',
350
+ },
351
+ SEPARADOR_VACÍO: {
352
+ kind: 'ejecución',
353
+ explanation:
354
+ '`dividir` y `reemplazar` necesitan un texto que buscar que no esté vacío. Para separar letra por letra, usa `caracteres(t)`.',
355
+ },
356
+ SIN_ENTRADA: {
357
+ kind: 'ejecución',
358
+ explanation:
359
+ '`leer_número()` no encontró más datos de entrada. Revisa cuántos datos espera el programa y cuántos se le dieron.',
360
+ },
361
+ SIN_ZUMBADORES: {
362
+ kind: 'ejecución',
363
+ explanation: 'El robot intentó recoger un zumbador donde no hay. Pregunta antes con `hay_zumbador()`.',
364
+ },
365
+ TEXTO_INMUTABLE: {
366
+ kind: 'ejecución',
367
+ explanation:
368
+ 'Los textos no se cambian letra por letra: `t[0] = "H"` es un error. Se construye un texto nuevo, por ejemplo uniendo partes con `+` o con `reemplazar`.',
369
+ },
370
+ TIPOS_MEZCLADOS: {
371
+ kind: 'ejecución',
372
+ explanation:
373
+ '`+` suma dos números o une dos textos, pero no mezcla: `"Total: " + 5` es un error. Convierte el número con `texto(5)`, o imprime los dos por separado: `imprimir("Total:", 5)`.',
374
+ },
375
+ TIPO_DE_ARGUMENTO: {
376
+ kind: 'ejecución',
377
+ explanation:
378
+ 'Un procedimiento de la biblioteca recibió un valor de un tipo que no acepta, como un texto donde esperaba un número. El mensaje dice qué esperaba y qué recibió.',
379
+ },
380
+ TIPO_INVÁLIDO: {
381
+ kind: 'ejecución',
382
+ explanation:
383
+ 'El operador no funciona con esos tipos, como `-` entre dos textos o `no` con un número. Revisa qué valor tiene cada lado.',
384
+ },
385
+ VARIABLE_DE_PARA: {
386
+ kind: 'ejecución',
387
+ explanation:
388
+ 'La variable de `para i desde … hasta …` avanza sola en cada vuelta, así que no se puede cambiar dentro del bucle. Si necesitas otro valor, guárdalo en una variable nueva; si necesitas controlar el avance a mano, usa `mientras`.',
389
+ },
390
+ VARIABLE_NO_DECLARADA: {
391
+ kind: 'ejecución',
392
+ explanation:
393
+ 'Se le asignó un valor a una variable que no existe. La primera vez se declara con `variable x = …`; después se cambia con `x = …`.',
394
+ },
395
+ VARIABLE_REPETIDA: {
396
+ kind: 'ejecución',
397
+ explanation:
398
+ 'La variable ya existe en este bloque. Para cambiar su valor se escribe `x = …`, sin `variable`. Un bloque interno sí puede declarar otra con el mismo nombre.',
399
+ },
400
+ ÍNDICE_FUERA_DE_RANGO: {
401
+ kind: 'ejecución',
402
+ explanation:
403
+ 'Las posiciones van de 0 a `longitud - 1` (o de -1 a `-longitud` desde el final). Un error clásico es usar `longitud(lista)` como índice: el último es `longitud(lista) - 1`.',
404
+ },
405
+ ÍNDICE_INVÁLIDO: {
406
+ kind: 'ejecución',
407
+ explanation:
408
+ 'La posición de una lista o texto tiene que ser un número entero. Las claves de texto son para diccionarios.',
409
+ },
410
+ ÍNDICE_NO_SOPORTADO: {
411
+ kind: 'ejecución',
412
+ explanation:
413
+ '`[ ]` funciona con listas, textos y diccionarios, y `.campo` solo con diccionarios. Revisa qué valor tiene la variable.',
414
+ },
415
+
416
+ // límite
417
+ DEMASIADAS_LLAMADAS: {
418
+ kind: 'límite',
419
+ explanation:
420
+ 'Hay demasiadas llamadas anidadas, casi siempre por una recursión sin caso base: el procedimiento se llama a sí mismo sin una condición que lo detenga.',
421
+ },
422
+ DEMASIADAS_OPERACIONES: {
423
+ kind: 'límite',
424
+ explanation:
425
+ 'El programa hizo más operaciones de las permitidas. Casi siempre es un bucle cuya condición nunca cambia; revisa que algo dentro del bucle lo acerque al final.',
426
+ },
427
+ DEMASIADA_SALIDA: {
428
+ kind: 'límite',
429
+ explanation:
430
+ 'El programa imprimió demasiado texto, casi siempre por un `imprimir` dentro de un bucle que no termina.',
431
+ },
432
+ };