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,1426 @@
1
+ import { describeType, isCallable, isDict, isList, repr, show, typeOf, WAIT_FOR_INPUT } from './values';
2
+ import { COSTS } from './costs';
3
+ import { equals } from './values';
4
+ import type { DictValue, ListValue, NativeContext, NativeValue, ProcedureDoc, Value } from './values';
5
+
6
+ type NativeFn = (args: Value[], context: NativeContext) => Value | typeof WAIT_FOR_INPUT;
7
+
8
+ /**
9
+ * Define un procedimiento de la biblioteca. La documentación es obligatoria: de ahí sale el catálogo que usan el
10
+ * sitio, el editor y las pruebas. El costo vive en `costs.ts`, en una sola tabla.
11
+ */
12
+ const define = (name: string, arity: NativeValue['arity'], doc: ProcedureDoc, call: NativeFn): NativeValue => {
13
+ const cost = COSTS[name];
14
+ return { kind: 'nativo', name, arity, call, doc, ...(cost ? { cost } : {}) };
15
+ };
16
+
17
+ /** Un nativo que solo ve la biblioteca escrita en Codexivo: no se documenta porque el programa no lo ve. */
18
+ const internal = (name: string, arity: NativeValue['arity'], call: NativeFn): NativeValue => ({
19
+ kind: 'nativo',
20
+ name,
21
+ arity,
22
+ call,
23
+ });
24
+
25
+ const expectNumber = (name: string, value: Value, context: NativeContext, position = ''): number => {
26
+ if (typeof value === 'number') return value;
27
+ throw context.fail(
28
+ 'TIPO_DE_ARGUMENTO',
29
+ `\`${name}\` espera un número${position}, pero recibió ${describeType(typeOf(value))} (${repr(value)}).`,
30
+ );
31
+ };
32
+
33
+ const expectList = (name: string, value: Value, context: NativeContext) => {
34
+ if (isList(value)) return value;
35
+ throw context.fail('TIPO_DE_ARGUMENTO', `\`${name}\` espera una lista, pero recibió ${describeType(typeOf(value))}.`);
36
+ };
37
+
38
+ const expectText = (name: string, value: Value, context: NativeContext, position = ''): string => {
39
+ if (typeof value === 'string') return value;
40
+ throw context.fail(
41
+ 'TIPO_DE_ARGUMENTO',
42
+ `\`${name}\` espera un texto${position}, pero recibió ${describeType(typeOf(value))} (${repr(value)}).`,
43
+ );
44
+ };
45
+
46
+ const expectInteger = (name: string, value: Value, context: NativeContext, position = ''): number => {
47
+ const number = expectNumber(name, value, context, position);
48
+ if (Number.isInteger(number)) return number;
49
+ throw context.fail(
50
+ 'TIPO_DE_ARGUMENTO',
51
+ `\`${name}\` espera un número entero${position}, pero recibió ${repr(number)}.`,
52
+ 'Redondea el valor con `redondear(…)`, `piso(…)` o `techo(…)`.',
53
+ );
54
+ };
55
+
56
+ const expectCount = (name: string, value: Value, context: NativeContext, position = ''): number => {
57
+ const count = expectInteger(name, value, context, position);
58
+ if (count >= 0 && count <= MAX_RANGE) return count;
59
+ throw context.fail(
60
+ 'TIPO_DE_ARGUMENTO',
61
+ count < 0
62
+ ? `\`${name}\` necesita una cantidad de 0 o más${position}, pero recibió ${count}.`
63
+ : `\`${name}\` produciría ${count} elementos; el máximo es ${MAX_RANGE}.`,
64
+ );
65
+ };
66
+
67
+ /** Mismo mensaje que el operador `[ ]` cuando el índice no cabe. */
68
+ const outOfRange = (index: number, length: number, list: boolean, context: NativeContext) => {
69
+ const what = list ? 'la lista' : 'el texto';
70
+ const unit = list ? (length === 1 ? 'elemento' : 'elementos') : length === 1 ? 'carácter' : 'caracteres';
71
+ return context.fail(
72
+ 'ÍNDICE_FUERA_DE_RANGO',
73
+ `El índice ${index} está fuera de ${what}, que tiene ${length} ${unit}.`,
74
+ length === 0
75
+ ? list
76
+ ? 'La lista está vacía.'
77
+ : 'El texto está vacío.'
78
+ : `Los índices válidos van de 0 a ${length - 1} (o de -1 a -${length} desde el final).`,
79
+ );
80
+ };
81
+
82
+ /**
83
+ * Posición real de `value` en algo de longitud `length`. Los negativos cuentan desde el final. Con
84
+ * `allowEnd`, `length` también vale (para `insertar` al final o como fin de `rebanar`).
85
+ */
86
+ const positionIn = (
87
+ name: string,
88
+ value: Value,
89
+ length: number,
90
+ list: boolean,
91
+ context: NativeContext,
92
+ allowEnd = false,
93
+ ): number => {
94
+ if (typeof value !== 'number' || !Number.isInteger(value)) {
95
+ throw context.fail('ÍNDICE_INVÁLIDO', `\`${name}\` necesita un índice entero, pero recibió ${repr(value)}.`);
96
+ }
97
+ const position = value < 0 ? length + value : value;
98
+ if (position < 0 || position > length || (position === length && !allowEnd)) {
99
+ throw outOfRange(value, length, list, context);
100
+ }
101
+ return position;
102
+ };
103
+
104
+ /** Copia una lista o un diccionario y todo lo que contiene, para no compartir casillas por accidente. */
105
+ const deepCopy = (value: Value, context: NativeContext, copies = new Map<Value, Value>()): Value => {
106
+ if (!isList(value) && !isDict(value)) return value;
107
+ const done = copies.get(value);
108
+ if (done) return done;
109
+ if (isList(value)) {
110
+ const copy = context.newList([]);
111
+ copies.set(value, copy);
112
+ for (const item of value.items) copy.items.push(deepCopy(item, context, copies));
113
+ return copy;
114
+ }
115
+ const copy = context.newDict(new Map());
116
+ copies.set(value, copy);
117
+ for (const [key, item] of value.entries) copy.entries.set(key, deepCopy(item, context, copies));
118
+ return copy;
119
+ };
120
+
121
+ const emptyListError = (name: string, context: NativeContext) =>
122
+ context.fail('LISTA_VACÍA', `\`${name}\` no puede tomar nada de una lista vacía.`);
123
+
124
+ /** Para `contiene` y `posición_de`, que con una lista ya se resolvieron: el valor tiene que ser un texto. */
125
+ const expectTextOrList = (name: string, value: Value, context: NativeContext): string => {
126
+ if (typeof value === 'string') return value;
127
+ throw context.fail(
128
+ 'TIPO_DE_ARGUMENTO',
129
+ `\`${name}\` espera un texto o una lista, pero recibió ${describeType(typeOf(value))}.`,
130
+ );
131
+ };
132
+
133
+ /** Inicio y fin (sin incluirlo) de `subtexto` y `rebanar`, con la misma convención que `[ ]`. */
134
+ const sliceBounds = (
135
+ name: string,
136
+ args: Value[],
137
+ length: number,
138
+ list: boolean,
139
+ context: NativeContext,
140
+ ): [number, number] => {
141
+ const start = positionIn(name, args[1]!, length, list, context, true);
142
+ const end = positionIn(name, args[2]!, length, list, context, true);
143
+ if (start > end) {
144
+ throw context.fail(
145
+ 'RANGO_INVÁLIDO',
146
+ `\`${name}\` necesita que el inicio (${repr(args[1]!)}) no esté después del fin (${repr(args[2]!)}).`,
147
+ 'El fin no se incluye: `subtexto("hola", 1, 3)` es `"ol"`.',
148
+ );
149
+ }
150
+ return [start, end];
151
+ };
152
+
153
+ /** Compara para `ordenar`: todos números o todos textos. */
154
+ const sortableKind = (list: ListValue, context: NativeContext): 'número' | 'texto' => {
155
+ const first = list.items[0];
156
+ const kind = typeof first === 'string' ? 'texto' : 'número';
157
+ for (const item of list.items) {
158
+ if (typeOf(item) !== kind) {
159
+ throw context.fail(
160
+ 'TIPO_DE_ARGUMENTO',
161
+ `\`ordenar\` necesita que todos los elementos sean números o todos textos, pero encontró ${describeType(typeOf(item))} (${repr(item)}).`,
162
+ );
163
+ }
164
+ }
165
+ return kind;
166
+ };
167
+
168
+ /** Una pila o una cola es una lista; los mensajes usan su propio vocabulario. */
169
+ const expectStructure = (name: string, kind: 'pila' | 'cola', value: Value, context: NativeContext): ListValue => {
170
+ if (isList(value)) return value;
171
+ throw context.fail(
172
+ 'TIPO_DE_ARGUMENTO',
173
+ `\`${name}\` espera una ${kind} (una lista), pero recibió ${describeType(typeOf(value))}.`,
174
+ `Una ${kind} empieza como una lista vacía: \`variable ${kind} = [];\`.`,
175
+ );
176
+ };
177
+
178
+ const emptyStructure = (name: string, kind: 'pila' | 'cola', context: NativeContext) =>
179
+ context.fail(
180
+ kind === 'pila' ? 'PILA_VACÍA' : 'COLA_VACÍA',
181
+ name === 'tope' || name === 'frente'
182
+ ? `Una ${kind} vacía no tiene \`${name}\`.`
183
+ : `No se puede \`${name}\` de una ${kind} vacía.`,
184
+ `Revisa antes con \`longitud(${kind}) > 0\`.`,
185
+ );
186
+
187
+ const expectDict = (name: string, value: Value, context: NativeContext): DictValue => {
188
+ if (isDict(value)) return value;
189
+ throw context.fail(
190
+ 'TIPO_DE_ARGUMENTO',
191
+ `\`${name}\` espera un diccionario, pero recibió ${describeType(typeOf(value))}.`,
192
+ );
193
+ };
194
+
195
+ const expectKey = (name: string, value: Value, context: NativeContext): string | number => {
196
+ if (typeof value === 'string' || typeof value === 'number') return value;
197
+ throw context.fail(
198
+ 'CLAVE_INVÁLIDA',
199
+ `\`${name}\` espera una clave de texto o número, pero recibió ${describeType(typeOf(value))}.`,
200
+ );
201
+ };
202
+
203
+ const numbersOf = (name: string, args: Value[], context: NativeContext): number[] => {
204
+ const values = args.length === 1 && isList(args[0]!) ? args[0].items : args;
205
+ if (values.length === 0) {
206
+ throw context.fail('LISTA_VACÍA', `\`${name}\` necesita al menos un número, pero la lista está vacía.`);
207
+ }
208
+ return values.map(value => expectNumber(name, value, context));
209
+ };
210
+
211
+ const parseNumber = (text: string): number | undefined => {
212
+ const trimmed = text.trim();
213
+ if (!/^[-+]?(\d+(\.\d*)?|\.\d+)$/.test(trimmed)) return undefined;
214
+ return Number(trimmed);
215
+ };
216
+
217
+ /** Redondeo escolar: los .5 se alejan del cero (2.5 → 3, -2.5 → -3). */
218
+ const roundHalfAway = (value: number, decimals: number): number => {
219
+ const factor = 10 ** decimals;
220
+ return (Math.sign(value) * Math.round(Math.abs(value) * factor)) / factor;
221
+ };
222
+
223
+ /** Más elementos que esto en una lista creada de golpe es casi siempre un error, y llenaría la memoria. */
224
+ const MAX_RANGE = 10_000_000;
225
+
226
+ const library: NativeValue[] = [
227
+ // entrada y salida
228
+ define(
229
+ 'imprimir',
230
+ [0, Infinity],
231
+ {
232
+ level: 'principiante',
233
+ category: 'entrada y salida',
234
+ params: [{ name: 'valor', type: 'cualquiera', optional: true }],
235
+ variadic: true,
236
+ returns: 'nulo',
237
+ description:
238
+ 'Escribe los valores separados por un espacio y termina la línea. Los textos se escriben sin comillas.',
239
+ examples: [{ code: 'imprimir("Hola,", "Ana", 16)', output: 'Hola, Ana 16\n' }],
240
+ },
241
+ (args, context) => {
242
+ context.write(`${args.map(show).join(' ')}\n`);
243
+ return null;
244
+ },
245
+ ),
246
+ define(
247
+ 'leer',
248
+ 0,
249
+ {
250
+ level: 'principiante',
251
+ category: 'entrada y salida',
252
+ params: [],
253
+ returns: 'texto | nulo',
254
+ description: 'Lee la siguiente línea de entrada como texto. Si ya no hay más entrada, regresa `nulo`.',
255
+ examples: [{ code: 'leer()', result: 'nulo' }],
256
+ },
257
+ (_args, context) => context.readLine(),
258
+ ),
259
+ define(
260
+ 'leer_número',
261
+ 0,
262
+ {
263
+ level: 'principiante',
264
+ category: 'entrada y salida',
265
+ params: [],
266
+ returns: 'número',
267
+ description: 'Lee la siguiente línea de entrada y la convierte en número. Si no es un número, es un error.',
268
+ examples: [{ code: 'variable edad = 0\nsi (falso) { edad = leer_número() }\nedad', result: '0' }],
269
+ },
270
+ (_args, context) => {
271
+ const line = context.readLine();
272
+ if (line === WAIT_FOR_INPUT) return line;
273
+ if (line === null) throw context.fail('SIN_ENTRADA', '`leer_número` no encontró más datos de entrada.');
274
+ const value = parseNumber(line);
275
+ if (value === undefined) {
276
+ throw context.fail('NO_ES_NÚMERO', `\`leer_número\` esperaba un número, pero leyó ${JSON.stringify(line)}.`);
277
+ }
278
+ return value;
279
+ },
280
+ ),
281
+
282
+ // tipos y conversiones
283
+ define(
284
+ 'tipo',
285
+ 1,
286
+ {
287
+ level: 'principiante',
288
+ category: 'tipos',
289
+ params: [{ name: 'x', type: 'cualquiera' }],
290
+ returns: 'texto',
291
+ description:
292
+ 'El nombre del tipo de un valor: "número", "texto", "lógico", "nulo", "lista", "diccionario", "dominio" o "procedimiento".',
293
+ examples: [
294
+ { code: 'tipo(3.5)', result: '"número"' },
295
+ { code: 'tipo([1, 2])', result: '"lista"' },
296
+ ],
297
+ },
298
+ args => typeOf(args[0]!),
299
+ ),
300
+ define(
301
+ 'texto',
302
+ 1,
303
+ {
304
+ level: 'principiante',
305
+ category: 'tipos',
306
+ params: [{ name: 'x', type: 'cualquiera' }],
307
+ returns: 'texto',
308
+ description: 'Convierte cualquier valor en texto, como lo escribiría `imprimir`.',
309
+ examples: [{ code: 'texto(42) + "!"', result: '"42!"' }],
310
+ },
311
+ args => show(args[0]!),
312
+ ),
313
+ define(
314
+ 'número',
315
+ 1,
316
+ {
317
+ level: 'principiante',
318
+ category: 'tipos',
319
+ params: [{ name: 'x', type: 'texto | número' }],
320
+ returns: 'número',
321
+ description: 'Convierte un texto en número. Si el texto no es un número, es un error.',
322
+ examples: [{ code: 'número("3.5") * 2', result: '7' }],
323
+ },
324
+ (args, context) => {
325
+ const value = args[0]!;
326
+ if (typeof value === 'number') return value;
327
+ if (typeof value === 'string') {
328
+ const parsed = parseNumber(value);
329
+ if (parsed !== undefined) return parsed;
330
+ throw context.fail('NO_ES_NÚMERO', `No se puede convertir ${JSON.stringify(value)} en número.`);
331
+ }
332
+ throw context.fail('TIPO_DE_ARGUMENTO', `No se puede convertir ${describeType(typeOf(value))} en número.`);
333
+ },
334
+ ),
335
+ define(
336
+ 'longitud',
337
+ 1,
338
+ {
339
+ level: 'principiante',
340
+ category: 'tipos',
341
+ params: [{ name: 'x', type: 'texto | lista | diccionario' }],
342
+ returns: 'número',
343
+ description: 'Cuántos caracteres tiene un texto, cuántos elementos una lista o cuántas claves un diccionario.',
344
+ examples: [
345
+ { code: 'longitud("año")', result: '3' },
346
+ { code: 'longitud([1, 2, 3])', result: '3' },
347
+ ],
348
+ },
349
+ (args, context) => {
350
+ const value = args[0]!;
351
+ if (typeof value === 'string') return [...value].length;
352
+ if (isList(value)) return value.items.length;
353
+ if (isDict(value)) return value.entries.size;
354
+ throw context.fail(
355
+ 'TIPO_DE_ARGUMENTO',
356
+ `\`longitud\` espera un texto, una lista o un diccionario, pero recibió ${describeType(typeOf(value))}.`,
357
+ );
358
+ },
359
+ ),
360
+
361
+ // matemáticas
362
+ define(
363
+ 'absoluto',
364
+ 1,
365
+ {
366
+ level: 'principiante',
367
+ category: 'matemáticas',
368
+ params: [{ name: 'x', type: 'número' }],
369
+ returns: 'número',
370
+ description: 'El valor absoluto: la distancia del número al 0.',
371
+ examples: [{ code: 'absoluto(-3)', result: '3' }],
372
+ },
373
+ (args, context) => Math.abs(expectNumber('absoluto', args[0]!, context)),
374
+ ),
375
+ define(
376
+ 'máximo',
377
+ [1, Infinity],
378
+ {
379
+ level: 'principiante',
380
+ category: 'matemáticas',
381
+ params: [{ name: 'números', type: 'número | lista' }],
382
+ variadic: true,
383
+ returns: 'número',
384
+ description: 'El mayor de varios números, o de los números de una lista.',
385
+ examples: [
386
+ { code: 'máximo(3, 9, 2)', result: '9' },
387
+ { code: 'máximo([4, 1])', result: '4' },
388
+ ],
389
+ },
390
+ (args, context) => Math.max(...numbersOf('máximo', args, context)),
391
+ ),
392
+ define(
393
+ 'mínimo',
394
+ [1, Infinity],
395
+ {
396
+ level: 'principiante',
397
+ category: 'matemáticas',
398
+ params: [{ name: 'números', type: 'número | lista' }],
399
+ variadic: true,
400
+ returns: 'número',
401
+ description: 'El menor de varios números, o de los números de una lista.',
402
+ examples: [{ code: 'mínimo(3, 9, 2)', result: '2' }],
403
+ },
404
+ (args, context) => Math.min(...numbersOf('mínimo', args, context)),
405
+ ),
406
+ define(
407
+ 'redondear',
408
+ [1, 2],
409
+ {
410
+ level: 'principiante',
411
+ category: 'matemáticas',
412
+ params: [
413
+ { name: 'x', type: 'número' },
414
+ { name: 'decimales', type: 'número', optional: true },
415
+ ],
416
+ returns: 'número',
417
+ description: 'Redondea al entero más cercano, o a cierta cantidad de decimales. Los .5 se alejan del cero.',
418
+ examples: [
419
+ { code: 'redondear(2.5)', result: '3' },
420
+ { code: 'redondear(3.14159, 2)', result: '3.14' },
421
+ ],
422
+ },
423
+ (args, context) => {
424
+ const value = expectNumber('redondear', args[0]!, context);
425
+ const decimals = args.length > 1 ? expectNumber('redondear', args[1]!, context, ' de decimales') : 0;
426
+ if (!Number.isInteger(decimals) || decimals < 0) {
427
+ throw context.fail(
428
+ 'TIPO_DE_ARGUMENTO',
429
+ 'La cantidad de decimales de `redondear` tiene que ser un entero positivo o 0.',
430
+ );
431
+ }
432
+ return roundHalfAway(value, decimals);
433
+ },
434
+ ),
435
+
436
+ // listas
437
+ define(
438
+ 'agregar',
439
+ 2,
440
+ {
441
+ level: 'principiante',
442
+ category: 'listas',
443
+ params: [
444
+ { name: 'lista', type: 'lista' },
445
+ { name: 'x', type: 'cualquiera' },
446
+ ],
447
+ returns: 'nulo',
448
+ mutates: true,
449
+ description: 'Agrega `x` al final de la lista.',
450
+ examples: [{ code: 'variable l = [1]\nagregar(l, 2)\nl', result: '[1, 2]' }],
451
+ },
452
+ (args, context) => {
453
+ expectList('agregar', args[0]!, context).items.push(args[1]!);
454
+ return null;
455
+ },
456
+ ),
457
+ define(
458
+ 'rango',
459
+ [2, 3],
460
+ {
461
+ level: 'intermedio',
462
+ category: 'listas',
463
+ params: [
464
+ { name: 'inicio', type: 'número' },
465
+ { name: 'fin', type: 'número' },
466
+ { name: 'paso', type: 'número', optional: true },
467
+ ],
468
+ returns: 'lista',
469
+ description: 'Lista de números de `inicio` a `fin`, incluyendo `fin`. Con un paso negativo cuenta hacia abajo.',
470
+ examples: [
471
+ { code: 'rango(1, 5)', result: '[1, 2, 3, 4, 5]' },
472
+ { code: 'rango(10, 0, -5)', result: '[10, 5, 0]' },
473
+ ],
474
+ },
475
+ (args, context) => {
476
+ const start = expectNumber('rango', args[0]!, context, ' como inicio');
477
+ const end = expectNumber('rango', args[1]!, context, ' como final');
478
+ const step = args.length > 2 ? expectNumber('rango', args[2]!, context, ' como paso') : 1;
479
+ if (step === 0) {
480
+ throw context.fail(
481
+ 'RANGO_PASO_CERO',
482
+ 'El paso de `rango` no puede ser 0: nunca llegaría al final.',
483
+ 'Usa un paso positivo para contar hacia arriba o uno negativo para contar hacia abajo.',
484
+ );
485
+ }
486
+ // Se tolera un error de redondeo pequeño para que `rango(0, 1, 0.1)` incluya el 1.
487
+ const count = Math.floor((end - start) / step + 1e-9) + 1;
488
+ if (!Number.isFinite(count)) throw context.fail('TIPO_DE_ARGUMENTO', '`rango` necesita números finitos.');
489
+ if (count > MAX_RANGE) {
490
+ throw context.fail(
491
+ 'RANGO_DEMASIADO_GRANDE',
492
+ `\`rango\` produciría ${count} números; el máximo es ${MAX_RANGE}.`,
493
+ 'Revisa el inicio, el final y el paso.',
494
+ );
495
+ }
496
+ return context.newList(Array.from({ length: Math.max(0, count) }, (_, index) => start + index * step));
497
+ },
498
+ ),
499
+ define(
500
+ 'índices',
501
+ 1,
502
+ {
503
+ level: 'intermedio',
504
+ category: 'listas',
505
+ params: [{ name: 'lista', type: 'lista | texto' }],
506
+ returns: 'lista',
507
+ description: 'Las posiciones válidas de una lista o texto, de 0 a `longitud - 1`.',
508
+ examples: [{ code: 'índices(["a", "b", "c"])', result: '[0, 1, 2]' }],
509
+ },
510
+ (args, context) => {
511
+ const value = args[0]!;
512
+ if (typeof value !== 'string' && !isList(value)) {
513
+ throw context.fail(
514
+ 'TIPO_DE_ARGUMENTO',
515
+ `\`índices\` espera una lista o un texto, pero recibió ${describeType(typeOf(value))}.`,
516
+ );
517
+ }
518
+ const length = typeof value === 'string' ? value.length : value.items.length;
519
+ return context.newList(Array.from({ length }, (_, index) => index));
520
+ },
521
+ ),
522
+ define(
523
+ 'quitar',
524
+ 2,
525
+ {
526
+ level: 'intermedio',
527
+ category: 'listas',
528
+ params: [
529
+ { name: 'lista', type: 'lista | diccionario' },
530
+ { name: 'posición', type: 'número | texto' },
531
+ ],
532
+ returns: 'cualquiera',
533
+ mutates: true,
534
+ description:
535
+ 'Quita el elemento en esa posición de una lista, o esa clave de un diccionario, y regresa lo que quitó.',
536
+ examples: [
537
+ { code: 'variable l = [1, 2, 3]\n[quitar(l, 0), l]', result: '[1, [2, 3]]' },
538
+ { code: 'variable d = { "a": 1 }\n[quitar(d, "a"), d]', result: '[1, {}]' },
539
+ ],
540
+ },
541
+ (args, context) => {
542
+ const container = args[0]!;
543
+ if (isDict(container)) {
544
+ const key = expectKey('quitar', args[1]!, context);
545
+ if (!container.entries.has(key)) {
546
+ throw context.fail(
547
+ 'CLAVE_INEXISTENTE',
548
+ `\`quitar\` no encontró la clave ${repr(key)} en el diccionario.`,
549
+ `Usa \`tiene(d, ${repr(key)})\` para preguntar antes de quitarla.`,
550
+ );
551
+ }
552
+ const value = container.entries.get(key)!;
553
+ container.entries.delete(key);
554
+ return value;
555
+ }
556
+ if (!isList(container)) {
557
+ throw context.fail(
558
+ 'TIPO_DE_ARGUMENTO',
559
+ `\`quitar\` espera una lista o un diccionario, pero recibió ${describeType(typeOf(container))}.`,
560
+ );
561
+ }
562
+ const index = args[1]!;
563
+ if (typeof index !== 'number' || !Number.isInteger(index)) {
564
+ throw context.fail('ÍNDICE_INVÁLIDO', `El índice tiene que ser un número entero, pero es ${repr(index)}.`);
565
+ }
566
+ const length = container.items.length;
567
+ const position = index < 0 ? length + index : index;
568
+ if (position < 0 || position >= length) {
569
+ throw context.fail(
570
+ 'ÍNDICE_FUERA_DE_RANGO',
571
+ `El índice ${index} está fuera de la lista, que tiene ${length} ${length === 1 ? 'elemento' : 'elementos'}.`,
572
+ length === 0
573
+ ? 'La lista está vacía.'
574
+ : `Los índices válidos van de 0 a ${length - 1} (o de -1 a -${length} desde el final).`,
575
+ );
576
+ }
577
+ return container.items.splice(position, 1)[0]!;
578
+ },
579
+ ),
580
+ define(
581
+ 'primero',
582
+ 1,
583
+ {
584
+ level: 'principiante',
585
+ category: 'listas',
586
+ params: [{ name: 'lista', type: 'lista' }],
587
+ returns: 'cualquiera',
588
+ description: 'El primer elemento de la lista.',
589
+ examples: [{ code: 'primero([7, 8, 9])', result: '7' }],
590
+ },
591
+ (args, context) => {
592
+ const list = expectList('primero', args[0]!, context);
593
+ if (list.items.length === 0)
594
+ throw context.fail('LISTA_VACÍA', '`primero` no puede tomar nada de una lista vacía.');
595
+ return list.items[0]!;
596
+ },
597
+ ),
598
+ define(
599
+ 'último',
600
+ 1,
601
+ {
602
+ level: 'principiante',
603
+ category: 'listas',
604
+ params: [{ name: 'lista', type: 'lista' }],
605
+ returns: 'cualquiera',
606
+ description: 'El último elemento de la lista.',
607
+ examples: [{ code: 'último([7, 8, 9])', result: '9' }],
608
+ },
609
+ (args, context) => {
610
+ const list = expectList('último', args[0]!, context);
611
+ if (list.items.length === 0)
612
+ throw context.fail('LISTA_VACÍA', '`último` no puede tomar nada de una lista vacía.');
613
+ return list.items[list.items.length - 1]!;
614
+ },
615
+ ),
616
+
617
+ // diccionarios
618
+ define(
619
+ 'claves',
620
+ 1,
621
+ {
622
+ level: 'intermedio',
623
+ category: 'diccionarios',
624
+ params: [{ name: 'd', type: 'diccionario' }],
625
+ returns: 'lista',
626
+ description: 'Una lista nueva con las claves del diccionario, en el orden en que se agregaron.',
627
+ examples: [{ code: 'claves({ "a": 1, "b": 2 })', result: '["a", "b"]' }],
628
+ },
629
+ (args, context) => context.newList([...expectDict('claves', args[0]!, context).entries.keys()]),
630
+ ),
631
+ define(
632
+ 'valores',
633
+ 1,
634
+ {
635
+ level: 'intermedio',
636
+ category: 'diccionarios',
637
+ params: [{ name: 'd', type: 'diccionario' }],
638
+ returns: 'lista',
639
+ description: 'Una lista nueva con los valores del diccionario, en el orden de sus claves.',
640
+ examples: [{ code: 'valores({ "a": 1, "b": 2 })', result: '[1, 2]' }],
641
+ },
642
+ (args, context) => context.newList([...expectDict('valores', args[0]!, context).entries.values()]),
643
+ ),
644
+ define(
645
+ 'tiene',
646
+ 2,
647
+ {
648
+ level: 'intermedio',
649
+ category: 'diccionarios',
650
+ params: [
651
+ { name: 'd', type: 'diccionario' },
652
+ { name: 'clave', type: 'texto | número' },
653
+ ],
654
+ returns: 'lógico',
655
+ description: '`verdadero` si el diccionario tiene esa clave.',
656
+ examples: [
657
+ { code: 'tiene({ "a": 1 }, "a")', result: 'verdadero' },
658
+ { code: 'tiene({ "a": 1 }, "b")', result: 'falso' },
659
+ ],
660
+ },
661
+ (args, context) => expectDict('tiene', args[0]!, context).entries.has(expectKey('tiene', args[1]!, context)),
662
+ ),
663
+
664
+ // matemáticas
665
+ define(
666
+ 'piso',
667
+ 1,
668
+ {
669
+ level: 'principiante',
670
+ category: 'matemáticas',
671
+ params: [{ name: 'x', type: 'número' }],
672
+ returns: 'número',
673
+ description: 'Redondea hacia abajo.',
674
+ examples: [
675
+ { code: 'piso(2.7)', result: '2' },
676
+ { code: 'piso(-2.1)', result: '-3' },
677
+ ],
678
+ },
679
+ (args, context) => Math.floor(expectNumber('piso', args[0]!, context)),
680
+ ),
681
+ define(
682
+ 'techo',
683
+ 1,
684
+ {
685
+ level: 'principiante',
686
+ category: 'matemáticas',
687
+ params: [{ name: 'x', type: 'número' }],
688
+ returns: 'número',
689
+ description: 'Redondea hacia arriba.',
690
+ examples: [{ code: 'techo(2.1)', result: '3' }],
691
+ },
692
+ (args, context) => Math.ceil(expectNumber('techo', args[0]!, context)),
693
+ ),
694
+ define(
695
+ 'entero',
696
+ 1,
697
+ {
698
+ level: 'principiante',
699
+ category: 'matemáticas',
700
+ params: [{ name: 'x', type: 'número' }],
701
+ returns: 'número',
702
+ description: 'Quita los decimales, sin redondear.',
703
+ examples: [{ code: 'entero(-2.7)', result: '-2' }],
704
+ },
705
+ (args, context) => Math.trunc(expectNumber('entero', args[0]!, context)),
706
+ ),
707
+ define(
708
+ 'raíz',
709
+ 1,
710
+ {
711
+ level: 'principiante',
712
+ category: 'matemáticas',
713
+ params: [{ name: 'x', type: 'número' }],
714
+ returns: 'número',
715
+ description: 'La raíz cuadrada. De un número negativo es un error.',
716
+ examples: [{ code: 'raíz(16)', result: '4' }],
717
+ },
718
+ (args, context) => {
719
+ const value = expectNumber('raíz', args[0]!, context);
720
+ if (value < 0) {
721
+ throw context.fail(
722
+ 'RAÍZ_DE_NEGATIVO',
723
+ `\`raíz\` no tiene un resultado real para ${repr(value)}.`,
724
+ 'La raíz cuadrada solo existe para números de 0 en adelante. ¿Quisiste usar `absoluto(…)` antes?',
725
+ );
726
+ }
727
+ return Math.sqrt(value);
728
+ },
729
+ ),
730
+ define(
731
+ 'aleatorio',
732
+ 2,
733
+ {
734
+ level: 'principiante',
735
+ category: 'matemáticas',
736
+ params: [
737
+ { name: 'mínimo', type: 'número' },
738
+ { name: 'máximo', type: 'número' },
739
+ ],
740
+ returns: 'número',
741
+ description:
742
+ 'Un entero al azar entre `mínimo` y `máximo`, los dos incluidos. Con la misma semilla da los mismos números.',
743
+ examples: [{ code: 'variable dado = aleatorio(1, 6)\ndado >= 1 y dado <= 6', result: 'verdadero' }],
744
+ },
745
+ (args, context) => {
746
+ const low = expectInteger('aleatorio', args[0]!, context, ' como mínimo');
747
+ const high = expectInteger('aleatorio', args[1]!, context, ' como máximo');
748
+ if (low > high) {
749
+ throw context.fail(
750
+ 'TIPO_DE_ARGUMENTO',
751
+ `\`aleatorio\` necesita que el mínimo (${low}) no sea mayor que el máximo (${high}).`,
752
+ );
753
+ }
754
+ return low + Math.floor(context.random() * (high - low + 1));
755
+ },
756
+ ),
757
+
758
+ // texto
759
+ define(
760
+ 'mayúsculas',
761
+ 1,
762
+ {
763
+ level: 'intermedio',
764
+ category: 'texto',
765
+ params: [{ name: 't', type: 'texto' }],
766
+ returns: 'texto',
767
+ description: 'El mismo texto en mayúsculas.',
768
+ examples: [{ code: 'mayúsculas("Ñandú")', result: '"ÑANDÚ"' }],
769
+ },
770
+ (args, context) => expectText('mayúsculas', args[0]!, context).toLocaleUpperCase('es'),
771
+ ),
772
+ define(
773
+ 'minúsculas',
774
+ 1,
775
+ {
776
+ level: 'intermedio',
777
+ category: 'texto',
778
+ params: [{ name: 't', type: 'texto' }],
779
+ returns: 'texto',
780
+ description: 'El mismo texto en minúsculas.',
781
+ examples: [{ code: 'minúsculas("ÁRBOL")', result: '"árbol"' }],
782
+ },
783
+ (args, context) => expectText('minúsculas', args[0]!, context).toLocaleLowerCase('es'),
784
+ ),
785
+ define(
786
+ 'recortar',
787
+ 1,
788
+ {
789
+ level: 'intermedio',
790
+ category: 'texto',
791
+ params: [{ name: 't', type: 'texto' }],
792
+ returns: 'texto',
793
+ description: 'El texto sin espacios ni saltos de línea al principio y al final.',
794
+ examples: [{ code: 'recortar(" hola ")', result: '"hola"' }],
795
+ },
796
+ (args, context) => expectText('recortar', args[0]!, context).trim(),
797
+ ),
798
+ define(
799
+ 'dividir',
800
+ 2,
801
+ {
802
+ level: 'intermedio',
803
+ category: 'texto',
804
+ params: [
805
+ { name: 't', type: 'texto' },
806
+ { name: 'separador', type: 'texto' },
807
+ ],
808
+ returns: 'lista',
809
+ description: 'Parte el texto en cada aparición del separador.',
810
+ examples: [{ code: 'dividir("a,b,c", ",")', result: '["a", "b", "c"]' }],
811
+ },
812
+ (args, context) => {
813
+ const text = expectText('dividir', args[0]!, context);
814
+ const separator = expectText('dividir', args[1]!, context, ' como separador');
815
+ if (separator === '') {
816
+ throw context.fail(
817
+ 'SEPARADOR_VACÍO',
818
+ '`dividir` necesita un separador que no esté vacío.',
819
+ 'Para separar un texto letra por letra usa `caracteres(t)`.',
820
+ );
821
+ }
822
+ return context.newList(text.split(separator));
823
+ },
824
+ ),
825
+ define(
826
+ 'unir',
827
+ 2,
828
+ {
829
+ level: 'intermedio',
830
+ category: 'texto',
831
+ params: [
832
+ { name: 'lista', type: 'lista' },
833
+ { name: 'separador', type: 'texto' },
834
+ ],
835
+ returns: 'texto',
836
+ description: 'Une los elementos de la lista en un texto, con el separador entre cada uno.',
837
+ examples: [{ code: 'unir(["a", "b", "c"], "-")', result: '"a-b-c"' }],
838
+ },
839
+ (args, context) => {
840
+ const list = expectList('unir', args[0]!, context);
841
+ const separator = expectText('unir', args[1]!, context, ' como separador');
842
+ return list.items.map(show).join(separator);
843
+ },
844
+ ),
845
+ define(
846
+ 'reemplazar',
847
+ 3,
848
+ {
849
+ level: 'intermedio',
850
+ category: 'texto',
851
+ params: [
852
+ { name: 't', type: 'texto' },
853
+ { name: 'buscar', type: 'texto' },
854
+ { name: 'poner', type: 'texto' },
855
+ ],
856
+ returns: 'texto',
857
+ description: 'Cambia cada aparición de `buscar` por `poner`.',
858
+ examples: [{ code: 'reemplazar("la casa", "a", "o")', result: '"lo coso"' }],
859
+ },
860
+ (args, context) => {
861
+ const text = expectText('reemplazar', args[0]!, context);
862
+ const search = expectText('reemplazar', args[1]!, context, ' para buscar');
863
+ const replacement = expectText('reemplazar', args[2]!, context, ' para poner');
864
+ if (search === '') {
865
+ throw context.fail('SEPARADOR_VACÍO', '`reemplazar` necesita un texto que buscar que no esté vacío.');
866
+ }
867
+ return text.replaceAll(search, replacement);
868
+ },
869
+ ),
870
+ define(
871
+ 'contiene',
872
+ 2,
873
+ {
874
+ level: 'intermedio',
875
+ category: 'texto',
876
+ params: [
877
+ { name: 't', type: 'texto | lista' },
878
+ { name: 'x', type: 'cualquiera' },
879
+ ],
880
+ returns: 'lógico',
881
+ description: 'En un texto, si aparece la parte buscada; en una lista, si está el valor (comparando por valor).',
882
+ examples: [
883
+ { code: 'contiene("murciélago", "cié")', result: 'verdadero' },
884
+ { code: 'contiene([1, 2], 3)', result: 'falso' },
885
+ ],
886
+ },
887
+ (args, context) => {
888
+ const container = args[0]!;
889
+ if (isList(container)) return container.items.some(item => equals(item, args[1]!));
890
+ return expectTextOrList('contiene', container, context).includes(
891
+ expectText('contiene', args[1]!, context, ' para buscar'),
892
+ );
893
+ },
894
+ ),
895
+ define(
896
+ 'posición_de',
897
+ 2,
898
+ {
899
+ level: 'intermedio',
900
+ category: 'texto',
901
+ params: [
902
+ { name: 't', type: 'texto | lista' },
903
+ { name: 'x', type: 'cualquiera' },
904
+ ],
905
+ returns: 'número',
906
+ description: 'Dónde aparece por primera vez la parte o el valor buscado, o `-1` si no está.',
907
+ examples: [
908
+ { code: 'posición_de("banana", "na")', result: '2' },
909
+ { code: 'posición_de([5, 6], 7)', result: '-1' },
910
+ ],
911
+ },
912
+ (args, context) => {
913
+ const container = args[0]!;
914
+ if (isList(container)) return container.items.findIndex(item => equals(item, args[1]!));
915
+ return expectTextOrList('posición_de', container, context).indexOf(
916
+ expectText('posición_de', args[1]!, context, ' para buscar'),
917
+ );
918
+ },
919
+ ),
920
+ define(
921
+ 'empieza_con',
922
+ 2,
923
+ {
924
+ level: 'intermedio',
925
+ category: 'texto',
926
+ params: [
927
+ { name: 't', type: 'texto' },
928
+ { name: 'inicio', type: 'texto' },
929
+ ],
930
+ returns: 'lógico',
931
+ description: '`verdadero` si el texto empieza con `inicio`.',
932
+ examples: [{ code: 'empieza_con("Codexivo", "Code")', result: 'verdadero' }],
933
+ },
934
+ (args, context) =>
935
+ expectText('empieza_con', args[0]!, context).startsWith(
936
+ expectText('empieza_con', args[1]!, context, ' como inicio'),
937
+ ),
938
+ ),
939
+ define(
940
+ 'termina_con',
941
+ 2,
942
+ {
943
+ level: 'intermedio',
944
+ category: 'texto',
945
+ params: [
946
+ { name: 't', type: 'texto' },
947
+ { name: 'final', type: 'texto' },
948
+ ],
949
+ returns: 'lógico',
950
+ description: '`verdadero` si el texto termina con `final`.',
951
+ examples: [{ code: 'termina_con("Codexivo", "vo")', result: 'verdadero' }],
952
+ },
953
+ (args, context) =>
954
+ expectText('termina_con', args[0]!, context).endsWith(
955
+ expectText('termina_con', args[1]!, context, ' como final'),
956
+ ),
957
+ ),
958
+ define(
959
+ 'subtexto',
960
+ 3,
961
+ {
962
+ level: 'intermedio',
963
+ category: 'texto',
964
+ params: [
965
+ { name: 't', type: 'texto' },
966
+ { name: 'inicio', type: 'número' },
967
+ { name: 'fin', type: 'número' },
968
+ ],
969
+ returns: 'texto',
970
+ description: 'Los caracteres desde `inicio` hasta antes de `fin`.',
971
+ examples: [{ code: 'subtexto("hola", 1, 3)', result: '"ol"' }],
972
+ },
973
+ (args, context) => {
974
+ const text = expectText('subtexto', args[0]!, context);
975
+ const [start, end] = sliceBounds('subtexto', args, text.length, false, context);
976
+ return text.slice(start, end);
977
+ },
978
+ ),
979
+ define(
980
+ 'caracteres',
981
+ 1,
982
+ {
983
+ level: 'intermedio',
984
+ category: 'texto',
985
+ params: [{ name: 't', type: 'texto' }],
986
+ returns: 'lista',
987
+ description: 'Una lista con cada carácter del texto.',
988
+ examples: [{ code: 'caracteres("año")', result: '["a", "ñ", "o"]' }],
989
+ },
990
+ (args, context) => context.newList([...expectText('caracteres', args[0]!, context)]),
991
+ ),
992
+ define(
993
+ 'código_de',
994
+ 1,
995
+ {
996
+ level: 'intermedio',
997
+ category: 'texto',
998
+ params: [{ name: 'c', type: 'texto' }],
999
+ returns: 'número',
1000
+ description: 'El código Unicode de un carácter.',
1001
+ examples: [{ code: 'código_de("A")', result: '65' }],
1002
+ },
1003
+ (args, context) => {
1004
+ const text = expectText('código_de', args[0]!, context);
1005
+ const characters = [...text];
1006
+ if (characters.length !== 1) {
1007
+ throw context.fail(
1008
+ 'TIPO_DE_ARGUMENTO',
1009
+ `\`código_de\` espera un solo carácter, pero recibió ${repr(text)}.`,
1010
+ 'Para un texto más largo, recorre `caracteres(t)`.',
1011
+ );
1012
+ }
1013
+ return characters[0]!.codePointAt(0)!;
1014
+ },
1015
+ ),
1016
+ define(
1017
+ 'carácter_de',
1018
+ 1,
1019
+ {
1020
+ level: 'intermedio',
1021
+ category: 'texto',
1022
+ params: [{ name: 'n', type: 'número' }],
1023
+ returns: 'texto',
1024
+ description: 'El carácter que tiene ese código Unicode.',
1025
+ examples: [{ code: 'carácter_de(241)', result: '"ñ"' }],
1026
+ },
1027
+ (args, context) => {
1028
+ const code = expectInteger('carácter_de', args[0]!, context);
1029
+ if (code < 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
1030
+ throw context.fail('TIPO_DE_ARGUMENTO', `${code} no es el código de ningún carácter.`);
1031
+ }
1032
+ return String.fromCodePoint(code);
1033
+ },
1034
+ ),
1035
+
1036
+ // listas que cambian
1037
+ define(
1038
+ 'insertar',
1039
+ 3,
1040
+ {
1041
+ level: 'intermedio',
1042
+ category: 'listas',
1043
+ params: [
1044
+ { name: 'lista', type: 'lista' },
1045
+ { name: 'posición', type: 'número' },
1046
+ { name: 'x', type: 'cualquiera' },
1047
+ ],
1048
+ returns: 'nulo',
1049
+ mutates: true,
1050
+ description: 'Pone `x` en esa posición y recorre los demás elementos.',
1051
+ examples: [{ code: 'variable l = [1, 3]\ninsertar(l, 1, 2)\nl', result: '[1, 2, 3]' }],
1052
+ },
1053
+ (args, context) => {
1054
+ const list = expectList('insertar', args[0]!, context);
1055
+ const position = positionIn('insertar', args[1]!, list.items.length, true, context, true);
1056
+ list.items.splice(position, 0, args[2]!);
1057
+ return null;
1058
+ },
1059
+ ),
1060
+ define(
1061
+ 'quitar_último',
1062
+ 1,
1063
+ {
1064
+ level: 'intermedio',
1065
+ category: 'listas',
1066
+ params: [{ name: 'lista', type: 'lista' }],
1067
+ returns: 'cualquiera',
1068
+ mutates: true,
1069
+ description: 'Quita el último elemento y lo regresa.',
1070
+ examples: [{ code: 'variable l = [1, 2]\n[quitar_último(l), l]', result: '[2, [1]]' }],
1071
+ },
1072
+ (args, context) => {
1073
+ const list = expectList('quitar_último', args[0]!, context);
1074
+ if (list.items.length === 0) throw emptyListError('quitar_último', context);
1075
+ return list.items.pop()!;
1076
+ },
1077
+ ),
1078
+ define(
1079
+ 'quitar_primero',
1080
+ 1,
1081
+ {
1082
+ level: 'intermedio',
1083
+ category: 'listas',
1084
+ params: [{ name: 'lista', type: 'lista' }],
1085
+ returns: 'cualquiera',
1086
+ mutates: true,
1087
+ description: 'Quita el primer elemento y lo regresa.',
1088
+ examples: [{ code: 'variable l = [1, 2]\n[quitar_primero(l), l]', result: '[1, [2]]' }],
1089
+ },
1090
+ (args, context) => {
1091
+ const list = expectList('quitar_primero', args[0]!, context);
1092
+ if (list.items.length === 0) throw emptyListError('quitar_primero', context);
1093
+ return list.items.shift()!;
1094
+ },
1095
+ ),
1096
+ define(
1097
+ 'vaciar',
1098
+ 1,
1099
+ {
1100
+ level: 'intermedio',
1101
+ category: 'listas',
1102
+ params: [{ name: 'lista', type: 'lista | diccionario' }],
1103
+ returns: 'nulo',
1104
+ mutates: true,
1105
+ description: 'Quita todos los elementos de una lista (o todas las claves de un diccionario).',
1106
+ examples: [{ code: 'variable l = [1, 2]\nvaciar(l)\nl', result: '[]' }],
1107
+ },
1108
+ (args, context) => {
1109
+ const container = args[0]!;
1110
+ if (isDict(container)) container.entries.clear();
1111
+ else expectList('vaciar', container, context).items.length = 0;
1112
+ return null;
1113
+ },
1114
+ ),
1115
+
1116
+ // listas nuevas
1117
+ define(
1118
+ 'rebanar',
1119
+ 3,
1120
+ {
1121
+ level: 'intermedio',
1122
+ category: 'listas',
1123
+ params: [
1124
+ { name: 'lista', type: 'lista' },
1125
+ { name: 'inicio', type: 'número' },
1126
+ { name: 'fin', type: 'número' },
1127
+ ],
1128
+ returns: 'lista',
1129
+ description: 'Una lista nueva con los elementos desde `inicio` hasta antes de `fin`.',
1130
+ examples: [{ code: 'rebanar([1, 2, 3, 4], 1, 3)', result: '[2, 3]' }],
1131
+ },
1132
+ (args, context) => {
1133
+ const list = expectList('rebanar', args[0]!, context);
1134
+ const [start, end] = sliceBounds('rebanar', args, list.items.length, true, context);
1135
+ return context.newList(list.items.slice(start, end));
1136
+ },
1137
+ ),
1138
+ define(
1139
+ 'invertir',
1140
+ 1,
1141
+ {
1142
+ level: 'intermedio',
1143
+ category: 'listas',
1144
+ params: [{ name: 'lista', type: 'lista | texto' }],
1145
+ returns: 'lista | texto',
1146
+ description: 'Una lista (o un texto) nueva al revés.',
1147
+ examples: [
1148
+ { code: 'invertir([1, 2, 3])', result: '[3, 2, 1]' },
1149
+ { code: 'invertir("hola")', result: '"aloh"' },
1150
+ ],
1151
+ },
1152
+ (args, context) => {
1153
+ const value = args[0]!;
1154
+ if (typeof value === 'string') return [...value].reverse().join('');
1155
+ if (isList(value)) return context.newList([...value.items].reverse());
1156
+ throw context.fail(
1157
+ 'TIPO_DE_ARGUMENTO',
1158
+ `\`invertir\` espera una lista o un texto, pero recibió ${describeType(typeOf(value))}.`,
1159
+ );
1160
+ },
1161
+ ),
1162
+ define(
1163
+ 'copiar',
1164
+ 1,
1165
+ {
1166
+ level: 'intermedio',
1167
+ category: 'listas',
1168
+ params: [{ name: 'lista', type: 'lista | diccionario' }],
1169
+ returns: 'lista | diccionario',
1170
+ description:
1171
+ 'Una lista o diccionario nuevo con los mismos elementos. Los elementos que son listas se siguen compartiendo.',
1172
+ examples: [{ code: 'variable a = [1]\nvariable b = copiar(a)\nagregar(b, 2)\n[a, b]', result: '[[1], [1, 2]]' }],
1173
+ },
1174
+ (args, context) => {
1175
+ const value = args[0]!;
1176
+ if (isDict(value)) return context.newDict(new Map(value.entries));
1177
+ return context.newList([...expectList('copiar', value, context).items]);
1178
+ },
1179
+ ),
1180
+ define(
1181
+ 'sumar',
1182
+ 1,
1183
+ {
1184
+ level: 'intermedio',
1185
+ category: 'listas',
1186
+ params: [{ name: 'lista', type: 'lista' }],
1187
+ returns: 'número',
1188
+ description: 'La suma de los números de la lista.',
1189
+ examples: [{ code: 'sumar([1, 2, 3.5])', result: '6.5' }],
1190
+ },
1191
+ (args, context) =>
1192
+ expectList('sumar', args[0]!, context).items.reduce<number>(
1193
+ (total, item) => total + expectNumber('sumar', item, context, ' en cada elemento'),
1194
+ 0,
1195
+ ),
1196
+ ),
1197
+ define(
1198
+ 'lista_de',
1199
+ 2,
1200
+ {
1201
+ level: 'intermedio',
1202
+ category: 'listas',
1203
+ params: [
1204
+ { name: 'n', type: 'número' },
1205
+ { name: 'valor', type: 'cualquiera' },
1206
+ ],
1207
+ returns: 'lista',
1208
+ description: 'Una lista con `n` copias de `valor`. Si el valor es una lista, cada casilla tiene su propia copia.',
1209
+ examples: [{ code: 'lista_de(3, 0)', result: '[0, 0, 0]' }],
1210
+ },
1211
+ (args, context) => {
1212
+ const count = expectCount('lista_de', args[0]!, context, ' como cantidad');
1213
+ return context.newList(Array.from({ length: count }, () => deepCopy(args[1]!, context)));
1214
+ },
1215
+ ),
1216
+ define(
1217
+ 'matriz',
1218
+ 3,
1219
+ {
1220
+ level: 'intermedio',
1221
+ category: 'listas',
1222
+ params: [
1223
+ { name: 'filas', type: 'número' },
1224
+ { name: 'columnas', type: 'número' },
1225
+ { name: 'valor', type: 'cualquiera' },
1226
+ ],
1227
+ returns: 'lista',
1228
+ description: 'Una lista de filas, cada una con `columnas` copias de `valor`.',
1229
+ examples: [{ code: 'matriz(2, 3, 0)', result: '[[0, 0, 0], [0, 0, 0]]' }],
1230
+ },
1231
+ (args, context) => {
1232
+ const rows = expectCount('matriz', args[0]!, context, ' como filas');
1233
+ const columns = expectCount('matriz', args[1]!, context, ' como columnas');
1234
+ if (rows * columns > MAX_RANGE) {
1235
+ throw context.fail(
1236
+ 'TIPO_DE_ARGUMENTO',
1237
+ `\`matriz\` produciría ${rows * columns} casillas; el máximo es ${MAX_RANGE}.`,
1238
+ );
1239
+ }
1240
+ return context.newList(
1241
+ Array.from({ length: rows }, () =>
1242
+ context.newList(Array.from({ length: columns }, () => deepCopy(args[2]!, context))),
1243
+ ),
1244
+ );
1245
+ },
1246
+ ),
1247
+
1248
+ // pilas y colas: listas con su propio vocabulario
1249
+ define(
1250
+ 'apilar',
1251
+ 2,
1252
+ {
1253
+ level: 'avanzado',
1254
+ category: 'estructuras',
1255
+ params: [
1256
+ { name: 'pila', type: 'lista' },
1257
+ { name: 'x', type: 'cualquiera' },
1258
+ ],
1259
+ returns: 'nulo',
1260
+ mutates: true,
1261
+ description: 'Pone `x` encima de la pila.',
1262
+ examples: [{ code: 'variable p = []\napilar(p, 1)\napilar(p, 2)\np', result: '[1, 2]' }],
1263
+ },
1264
+ (args, context) => {
1265
+ expectStructure('apilar', 'pila', args[0]!, context).items.push(args[1]!);
1266
+ return null;
1267
+ },
1268
+ ),
1269
+ define(
1270
+ 'desapilar',
1271
+ 1,
1272
+ {
1273
+ level: 'avanzado',
1274
+ category: 'estructuras',
1275
+ params: [{ name: 'pila', type: 'lista' }],
1276
+ returns: 'cualquiera',
1277
+ mutates: true,
1278
+ description: 'Quita el elemento de encima de la pila y lo regresa.',
1279
+ examples: [{ code: 'variable p = [1, 2]\n[desapilar(p), p]', result: '[2, [1]]' }],
1280
+ },
1281
+ (args, context) => {
1282
+ const stack = expectStructure('desapilar', 'pila', args[0]!, context);
1283
+ if (stack.items.length === 0) throw emptyStructure('desapilar', 'pila', context);
1284
+ return stack.items.pop()!;
1285
+ },
1286
+ ),
1287
+ define(
1288
+ 'tope',
1289
+ 1,
1290
+ {
1291
+ level: 'avanzado',
1292
+ category: 'estructuras',
1293
+ params: [{ name: 'pila', type: 'lista' }],
1294
+ returns: 'cualquiera',
1295
+ description: 'El elemento de encima de la pila, sin quitarlo.',
1296
+ examples: [{ code: 'tope([1, 2])', result: '2' }],
1297
+ },
1298
+ (args, context) => {
1299
+ const stack = expectStructure('tope', 'pila', args[0]!, context);
1300
+ if (stack.items.length === 0) throw emptyStructure('tope', 'pila', context);
1301
+ return stack.items[stack.items.length - 1]!;
1302
+ },
1303
+ ),
1304
+ define(
1305
+ 'encolar',
1306
+ 2,
1307
+ {
1308
+ level: 'avanzado',
1309
+ category: 'estructuras',
1310
+ params: [
1311
+ { name: 'cola', type: 'lista' },
1312
+ { name: 'x', type: 'cualquiera' },
1313
+ ],
1314
+ returns: 'nulo',
1315
+ mutates: true,
1316
+ description: 'Pone `x` al final de la cola.',
1317
+ examples: [{ code: 'variable c = []\nencolar(c, 1)\nencolar(c, 2)\nc', result: '[1, 2]' }],
1318
+ },
1319
+ (args, context) => {
1320
+ expectStructure('encolar', 'cola', args[0]!, context).items.push(args[1]!);
1321
+ return null;
1322
+ },
1323
+ ),
1324
+ define(
1325
+ 'desencolar',
1326
+ 1,
1327
+ {
1328
+ level: 'avanzado',
1329
+ category: 'estructuras',
1330
+ params: [{ name: 'cola', type: 'lista' }],
1331
+ returns: 'cualquiera',
1332
+ mutates: true,
1333
+ description: 'Quita el primer elemento de la cola y lo regresa.',
1334
+ examples: [{ code: 'variable c = [1, 2]\n[desencolar(c), c]', result: '[1, [2]]' }],
1335
+ },
1336
+ (args, context) => {
1337
+ const queue = expectStructure('desencolar', 'cola', args[0]!, context);
1338
+ if (queue.items.length === 0) throw emptyStructure('desencolar', 'cola', context);
1339
+ return queue.items.shift()!;
1340
+ },
1341
+ ),
1342
+ define(
1343
+ 'frente',
1344
+ 1,
1345
+ {
1346
+ level: 'avanzado',
1347
+ category: 'estructuras',
1348
+ params: [{ name: 'cola', type: 'lista' }],
1349
+ returns: 'cualquiera',
1350
+ description: 'El primer elemento de la cola, sin quitarlo.',
1351
+ examples: [{ code: 'frente([1, 2])', result: '1' }],
1352
+ },
1353
+ (args, context) => {
1354
+ const queue = expectStructure('frente', 'cola', args[0]!, context);
1355
+ if (queue.items.length === 0) throw emptyStructure('frente', 'cola', context);
1356
+ return queue.items[0]!;
1357
+ },
1358
+ ),
1359
+ ];
1360
+
1361
+ /** Constantes de la biblioteca. Una variable del usuario con el mismo nombre las oculta. */
1362
+ export const constants: Readonly<Record<string, Value>> = {
1363
+ PI: Math.PI,
1364
+ };
1365
+
1366
+ /** Documentación de cada constante, para el catálogo. */
1367
+ export const constantDocs: Readonly<Record<string, { level: 'principiante'; description: string }>> = {
1368
+ PI: { level: 'principiante', description: 'La razón entre el perímetro de un círculo y su diámetro: 3.14159…' },
1369
+ };
1370
+
1371
+ /** Quita los acentos agudos y la diéresis, pero conserva la ñ. */
1372
+ export const withoutAccents = (name: string): string =>
1373
+ name.replace(/[áéíóúü]/g, character => ({ á: 'a', é: 'e', í: 'i', ó: 'o', ú: 'u', ü: 'u' })[character] ?? character);
1374
+
1375
+ /** Biblioteca estándar. Cada nombre con acento también se acepta sin él (`máximo` y `maximo`). */
1376
+ export const natives: Record<string, NativeValue> = (() => {
1377
+ const table: Record<string, NativeValue> = {};
1378
+ for (const native of library) table[native.name] = native;
1379
+ for (const native of library) {
1380
+ const alias = withoutAccents(native.name);
1381
+ if (!table[alias]) table[alias] = native;
1382
+ }
1383
+ return table;
1384
+ })();
1385
+
1386
+ /**
1387
+ * Nativos que solo puede usar la biblioteca escrita en Codexivo (`prelude.ts`): validan argumentos con los
1388
+ * mismos mensajes que el resto de la biblioteca y ordenan sin comparador.
1389
+ */
1390
+ export const internals: Record<string, NativeValue> = Object.fromEntries(
1391
+ [
1392
+ internal('revisar_lista', 2, (args, context) => {
1393
+ expectList(args[0] as string, args[1]!, context);
1394
+ return null;
1395
+ }),
1396
+ internal('revisar_procedimiento', 2, (args, context) => {
1397
+ const value = args[1]!;
1398
+ if (isCallable(value)) return null;
1399
+ throw context.fail(
1400
+ 'TIPO_DE_ARGUMENTO',
1401
+ `\`${args[0] as string}\` espera un procedimiento, pero recibió ${describeType(typeOf(value))} (${repr(value)}).`,
1402
+ 'Pasa el nombre del procedimiento sin paréntesis (`mapear(lista, doble)`), o escribe uno: `procedimiento(x) { regresa x * 2 }`.',
1403
+ );
1404
+ }),
1405
+ internal('revisar_logico', 2, (args, context) => {
1406
+ const value = args[1]!;
1407
+ if (typeof value === 'boolean') return value;
1408
+ throw context.fail(
1409
+ 'TIPO_DE_ARGUMENTO',
1410
+ `El procedimiento que recibió \`${args[0] as string}\` tiene que regresar \`verdadero\` o \`falso\`, pero regresó ${repr(value)}.`,
1411
+ args[0] === 'ordenar'
1412
+ ? 'El comparador de `ordenar(lista, va_antes)` responde «¿va `a` antes que `b`?»: `procedimiento(a, b) { regresa a < b }`.'
1413
+ : '¿Le falta un `regresa` con una comparación, como `regresa x > 0`?',
1414
+ );
1415
+ }),
1416
+ internal('ordenar_natural', 1, (args, context) => {
1417
+ const list = expectList('ordenar', args[0]!, context);
1418
+ const kind = sortableKind(list, context);
1419
+ // `sort` de JavaScript es estable; los textos se comparan carácter por carácter, igual que `<`.
1420
+ const items = [...list.items].sort((a, b) =>
1421
+ kind === 'número' ? (a as number) - (b as number) : a! < b! ? -1 : a! > b! ? 1 : 0,
1422
+ );
1423
+ return context.newList(items);
1424
+ }),
1425
+ ].map(native => [native.name, native]),
1426
+ );