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,230 @@
1
+ import { parse } from '../syntax/parser';
2
+ import { compileProcedure } from './compiler';
3
+ import { COSTS } from './costs';
4
+ import type { Chunk } from './compiler';
5
+ import { Scope } from './values';
6
+ import type { ProcedureDoc, ProcedureValue } from './values';
7
+
8
+ /**
9
+ * Los procedimientos de la biblioteca que reciben procedimientos están escritos en Codexivo. Así, cuando
10
+ * llaman a `f`, la llamada pasa por la máquina como cualquier otra: se puede pausar dentro de `f`, cuenta
11
+ * para el límite de llamadas, emite sus eventos y sus operaciones se cuentan igual que siempre.
12
+ *
13
+ * Su propio código se compila como oculto (`Chunk.hidden`): no pausa ni emite pasos, y en lugar de contar
14
+ * cada instrucción se cobra lo que dice `costs.ts`. Los `revisar_*` y `ordenar_natural` son nativos internos
15
+ * que solo se ven desde aquí (`internals` en `builtins.ts`).
16
+ */
17
+ const SOURCE = `
18
+ procedimiento mapear(lista, f) {
19
+ revisar_lista("mapear", lista);
20
+ revisar_procedimiento("mapear", f);
21
+ variable resultado = [];
22
+ para_cada elemento en lista {
23
+ agregar(resultado, f(elemento));
24
+ }
25
+ regresa resultado;
26
+ }
27
+
28
+ procedimiento filtrar(lista, f) {
29
+ revisar_lista("filtrar", lista);
30
+ revisar_procedimiento("filtrar", f);
31
+ variable resultado = [];
32
+ para_cada elemento en lista {
33
+ si (revisar_logico("filtrar", f(elemento))) {
34
+ agregar(resultado, elemento);
35
+ }
36
+ }
37
+ regresa resultado;
38
+ }
39
+
40
+ procedimiento reducir(lista, f, inicial) {
41
+ revisar_lista("reducir", lista);
42
+ revisar_procedimiento("reducir", f);
43
+ variable acumulado = inicial;
44
+ para_cada elemento en lista {
45
+ acumulado = f(acumulado, elemento);
46
+ }
47
+ regresa acumulado;
48
+ }
49
+
50
+ procedimiento alguno(lista, f) {
51
+ revisar_lista("alguno", lista);
52
+ revisar_procedimiento("alguno", f);
53
+ para_cada elemento en lista {
54
+ si (revisar_logico("alguno", f(elemento))) {
55
+ regresa verdadero;
56
+ }
57
+ }
58
+ regresa falso;
59
+ }
60
+
61
+ procedimiento todos(lista, f) {
62
+ revisar_lista("todos", lista);
63
+ revisar_procedimiento("todos", f);
64
+ para_cada elemento en lista {
65
+ si (no revisar_logico("todos", f(elemento))) {
66
+ regresa falso;
67
+ }
68
+ }
69
+ regresa verdadero;
70
+ }
71
+
72
+ procedimiento buscar(lista, f) {
73
+ revisar_lista("buscar", lista);
74
+ revisar_procedimiento("buscar", f);
75
+ para_cada elemento en lista {
76
+ si (revisar_logico("buscar", f(elemento))) {
77
+ regresa elemento;
78
+ }
79
+ }
80
+ regresa nulo;
81
+ }
82
+
83
+ procedimiento ordenar(lista, va_antes) {
84
+ revisar_lista("ordenar", lista);
85
+ si (va_antes == nulo) {
86
+ regresa ordenar_natural(lista);
87
+ }
88
+ revisar_procedimiento("ordenar", va_antes);
89
+
90
+ // Ordenamiento por mezcla: estable, porque solo adelanta un elemento de la derecha si va antes.
91
+ procedimiento mezclar(parte) {
92
+ si (longitud(parte) <= 1) {
93
+ regresa copiar(parte);
94
+ }
95
+ variable medio = longitud(parte) div 2;
96
+ variable izquierda = mezclar(rebanar(parte, 0, medio));
97
+ variable derecha = mezclar(rebanar(parte, medio, longitud(parte)));
98
+ variable resultado = [];
99
+ variable i = 0;
100
+ variable j = 0;
101
+ mientras (i < longitud(izquierda) y j < longitud(derecha)) {
102
+ si (revisar_logico("ordenar", va_antes(derecha[j], izquierda[i]))) {
103
+ agregar(resultado, derecha[j]);
104
+ j++;
105
+ } si_no {
106
+ agregar(resultado, izquierda[i]);
107
+ i++;
108
+ }
109
+ }
110
+ mientras (i < longitud(izquierda)) {
111
+ agregar(resultado, izquierda[i]);
112
+ i++;
113
+ }
114
+ mientras (j < longitud(derecha)) {
115
+ agregar(resultado, derecha[j]);
116
+ j++;
117
+ }
118
+ regresa resultado;
119
+ }
120
+ regresa mezclar(lista);
121
+ }
122
+ `;
123
+
124
+ const each = (): ProcedureDoc['params'] => [
125
+ { name: 'lista', type: 'lista' },
126
+ { name: 'f', type: 'procedimiento' },
127
+ ];
128
+
129
+ /** Documentación de cada procedimiento de `SOURCE`, para el catálogo. */
130
+ export const libraryDocs: Readonly<Record<string, ProcedureDoc>> = {
131
+ mapear: {
132
+ level: 'avanzado',
133
+ category: 'procedimientos',
134
+ params: each(),
135
+ returns: 'lista',
136
+ description: 'Una lista nueva con el resultado de `f(x)` para cada elemento.',
137
+ examples: [{ code: 'mapear([1, 2, 3], procedimiento(x) { regresa x * 2 })', result: '[2, 4, 6]' }],
138
+ },
139
+ filtrar: {
140
+ level: 'avanzado',
141
+ category: 'procedimientos',
142
+ params: each(),
143
+ returns: 'lista',
144
+ description: 'Una lista nueva con los elementos para los que `f(x)` es `verdadero`.',
145
+ examples: [{ code: 'filtrar([1, 2, 3, 4], procedimiento(x) { regresa x mod 2 == 0 })', result: '[2, 4]' }],
146
+ },
147
+ reducir: {
148
+ level: 'avanzado',
149
+ category: 'procedimientos',
150
+ params: [...each(), { name: 'inicial', type: 'cualquiera' }],
151
+ returns: 'cualquiera',
152
+ description: 'Junta la lista en un solo valor: empieza con `inicial` y en cada elemento hace `f(acumulado, x)`.',
153
+ examples: [{ code: 'reducir([1, 2, 3], procedimiento(total, x) { regresa total + x }, 0)', result: '6' }],
154
+ },
155
+ alguno: {
156
+ level: 'avanzado',
157
+ category: 'procedimientos',
158
+ params: each(),
159
+ returns: 'lógico',
160
+ description: '`verdadero` si `f(x)` es `verdadero` para al menos un elemento.',
161
+ examples: [{ code: 'alguno([1, 5], procedimiento(x) { regresa x > 3 })', result: 'verdadero' }],
162
+ },
163
+ todos: {
164
+ level: 'avanzado',
165
+ category: 'procedimientos',
166
+ params: each(),
167
+ returns: 'lógico',
168
+ description: '`verdadero` si `f(x)` es `verdadero` para todos los elementos.',
169
+ examples: [{ code: 'todos([1, 5], procedimiento(x) { regresa x > 3 })', result: 'falso' }],
170
+ },
171
+ buscar: {
172
+ level: 'avanzado',
173
+ category: 'procedimientos',
174
+ params: each(),
175
+ returns: 'cualquiera',
176
+ description: 'El primer elemento para el que `f(x)` es `verdadero`, o `nulo` si no hay.',
177
+ examples: [{ code: 'buscar([1, 5, 7], procedimiento(x) { regresa x > 3 })', result: '5' }],
178
+ },
179
+ ordenar: {
180
+ level: 'intermedio',
181
+ category: 'listas',
182
+ params: [
183
+ { name: 'lista', type: 'lista' },
184
+ { name: 'va_antes', type: 'procedimiento', optional: true },
185
+ ],
186
+ returns: 'lista',
187
+ description:
188
+ 'Una lista nueva ordenada. Sin comparador, todos tienen que ser números o todos textos; `va_antes(a, b)` responde «¿va `a` antes que `b`?». Es estable.',
189
+ examples: [
190
+ { code: 'ordenar([3, 1, 2])', result: '[1, 2, 3]' },
191
+ { code: 'ordenar([3, 1, 2], procedimiento(a, b) { regresa a > b })', result: '[3, 2, 1]' },
192
+ ],
193
+ },
194
+ };
195
+
196
+ /** Parámetros que se pueden omitir (llegan como `nulo`). */
197
+ const OPTIONAL: Readonly<Record<string, number>> = { ordenar: 1 };
198
+
199
+ const compileLibrary = (): Map<string, Chunk> => {
200
+ const { program, errors } = parse(SOURCE);
201
+ if (errors.length > 0) throw new Error(`La biblioteca en Codexivo tiene errores: ${errors[0]!.message}`);
202
+ const chunks = new Map<string, Chunk>();
203
+ for (const statement of program.statements) {
204
+ if (statement.kind !== 'FunctionDeclaration') continue;
205
+ const chunk = compileProcedure(statement.function, true);
206
+ const optional = OPTIONAL[statement.name.name];
207
+ if (optional !== undefined) chunk.minParams = optional;
208
+ chunks.set(statement.name.name, chunk);
209
+ }
210
+ return chunks;
211
+ };
212
+
213
+ /**
214
+ * Los procedimientos de la biblioteca escritos en Codexivo, por nombre. Son inmutables, así que todas las
215
+ * máquinas los comparten. Sus `id` son negativos para no mover los de los valores del programa.
216
+ */
217
+ export const libraryProcedures: Readonly<Record<string, ProcedureValue>> = (() => {
218
+ const scope = new Scope();
219
+ const table: Record<string, ProcedureValue> = {};
220
+ let id = -1;
221
+ for (const [name, chunk] of compileLibrary()) {
222
+ const procedure: ProcedureValue = { kind: 'procedimiento', id: id--, name, chunk, closure: scope };
223
+ scope.variables.set(name, procedure);
224
+ table[name] = procedure;
225
+ }
226
+ return table;
227
+ })();
228
+
229
+ /** Costo de llamar a un procedimiento de la biblioteca escrito en Codexivo. */
230
+ export const libraryCost = (name: string) => COSTS[name];
@@ -0,0 +1,33 @@
1
+ const distance = (a: string, b: string): number => {
2
+ const previous = Array.from({ length: b.length + 1 }, (_, index) => index);
3
+ for (let i = 1; i <= a.length; i++) {
4
+ let diagonal = previous[0]!;
5
+ previous[0] = i;
6
+ for (let j = 1; j <= b.length; j++) {
7
+ const above = previous[j]!;
8
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
9
+ previous[j] = Math.min(previous[j]! + 1, previous[j - 1]! + 1, diagonal + cost);
10
+ diagonal = above;
11
+ }
12
+ }
13
+ return previous[b.length]!;
14
+ };
15
+
16
+ const fold = (text: string): string => text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
17
+
18
+ /** El nombre más parecido a `name` entre `candidates`, si hay uno razonablemente cerca. */
19
+ export const suggestName = (name: string, candidates: Iterable<string>): string | undefined => {
20
+ const target = fold(name);
21
+ const limit = target.length <= 3 ? 1 : 2;
22
+ let best: string | undefined;
23
+ let bestDistance = Infinity;
24
+ for (const candidate of new Set(candidates)) {
25
+ if (candidate === name) continue;
26
+ const current = distance(target, fold(candidate));
27
+ if (current <= limit && current < bestDistance) {
28
+ best = candidate;
29
+ bestDistance = current;
30
+ }
31
+ }
32
+ return best;
33
+ };
@@ -0,0 +1,241 @@
1
+ import type { CodexivoError } from '../errors';
2
+ import type { RobotEvent } from '../world';
3
+ import type { Chunk } from './compiler';
4
+
5
+ /**
6
+ * Valores en tiempo de ejecución. Números, textos, lógicos y `nulo` son primitivos de JavaScript;
7
+ * listas y procedimientos son objetos con un `id` para poder dibujar cuándo dos variables
8
+ * comparten el mismo valor.
9
+ */
10
+ export type Value =
11
+ number | string | boolean | null | ListValue | DictValue | DomainValue | ProcedureValue | NativeValue;
12
+
13
+ export interface ListValue {
14
+ kind: 'lista';
15
+ id: number;
16
+ items: Value[];
17
+ }
18
+
19
+ /** Clave de un diccionario: un texto o un número. */
20
+ export type DictKey = string | number;
21
+
22
+ /** Diccionario: conserva el orden en que se agregaron las claves. */
23
+ export interface DictValue {
24
+ kind: 'diccionario';
25
+ id: number;
26
+ entries: Map<DictKey, Value>;
27
+ }
28
+
29
+ /**
30
+ * Un `dominio`: un espacio de nombres. Sus miembros son las variables de `scope`; desde fuera se usan con `.` y
31
+ * solo se pueden cambiar los que se declararon con `variable` (`writable`).
32
+ */
33
+ export interface DomainValue {
34
+ kind: 'dominio';
35
+ id: number;
36
+ name: string;
37
+ scope: Scope;
38
+ writable: ReadonlySet<string>;
39
+ }
40
+
41
+ export interface ProcedureValue {
42
+ kind: 'procedimiento';
43
+ id: number;
44
+ name?: string;
45
+ chunk: Chunk;
46
+ closure: Scope;
47
+ }
48
+
49
+ /** Contexto que la máquina le da a los procedimientos nativos. */
50
+ export interface NativeContext {
51
+ write(text: string): void;
52
+ /** Siguiente línea de entrada, `null` si ya no hay, o `WAIT_FOR_INPUT` si hay que pedirla. */
53
+ readLine(): string | null | typeof WAIT_FOR_INPUT;
54
+ newList(items: Value[]): ListValue;
55
+ newDict(entries: Map<DictKey, Value>): DictValue;
56
+ /** Número pseudoaleatorio en [0, 1) del generador con semilla de la máquina. */
57
+ random(): number;
58
+ /** Error de ejecución ubicado en la llamada actual. */
59
+ fail(code: string, message: string, hint?: string): CodexivoError;
60
+ /** Emite un evento propio del procedimiento (el modo mundo emite `robot`), ubicado en la llamada actual. */
61
+ emit(event: RobotEvent): void;
62
+ }
63
+
64
+ export const WAIT_FOR_INPUT: unique symbol = Symbol('esperar-entrada');
65
+
66
+ export type ProcedureLevel = 'principiante' | 'intermedio' | 'avanzado';
67
+
68
+ export type ProcedureCategory =
69
+ | 'entrada y salida'
70
+ | 'tipos'
71
+ | 'matemáticas'
72
+ | 'texto'
73
+ | 'listas'
74
+ | 'diccionarios'
75
+ | 'procedimientos'
76
+ | 'estructuras'
77
+ | 'mundo';
78
+
79
+ /** Ejemplo ejecutable de un procedimiento: `result` es el valor final como se escribiría en código. */
80
+ export interface ProcedureExample {
81
+ code: string;
82
+ result?: string;
83
+ output?: string;
84
+ }
85
+
86
+ /** Documentación de un procedimiento de la biblioteca; de aquí sale el catálogo (`catalog.ts`). */
87
+ export interface ProcedureDoc {
88
+ level: ProcedureLevel;
89
+ category: ProcedureCategory;
90
+ /** Tipos con los nombres de `tipo(x)`, `cualquiera`, o varios separados por ` | `. */
91
+ params: Array<{ name: string; type: string; optional?: boolean }>;
92
+ /** `true` si el último parámetro se puede repetir (`imprimir(a, b, …)`). */
93
+ variadic?: boolean;
94
+ /** Lo que regresa; `nulo` si no regresa nada útil. */
95
+ returns: string;
96
+ /** `true` si cambia su primer argumento. */
97
+ mutates?: boolean;
98
+ description: string;
99
+ examples: ProcedureExample[];
100
+ }
101
+
102
+ export interface NativeValue {
103
+ kind: 'nativo';
104
+ name: string;
105
+ /** Documentación para el catálogo. Los nativos internos de la biblioteca no la tienen. */
106
+ doc?: ProcedureDoc;
107
+ /** Cantidad de argumentos: exacta, o `[mínimo, máximo]` (`máximo` puede ser `Infinity`). */
108
+ arity: number | [number, number];
109
+ call(args: Value[], context: NativeContext): Value | typeof WAIT_FOR_INPUT;
110
+ /**
111
+ * Cuántas operaciones debería costar una llamada con estos argumentos (§10). Sin `cost`, cuesta 1.
112
+ * Se registra aquí para que la tabla de costos viva junto a cada procedimiento.
113
+ */
114
+ cost?: (args: Value[]) => number;
115
+ }
116
+
117
+ export type TypeName = 'número' | 'texto' | 'lógico' | 'nulo' | 'lista' | 'diccionario' | 'dominio' | 'procedimiento';
118
+
119
+ export const typeOf = (value: Value): TypeName => {
120
+ if (value === null) return 'nulo';
121
+ switch (typeof value) {
122
+ case 'number':
123
+ return 'número';
124
+ case 'string':
125
+ return 'texto';
126
+ case 'boolean':
127
+ return 'lógico';
128
+ default:
129
+ return value.kind === 'lista' || value.kind === 'diccionario' || value.kind === 'dominio'
130
+ ? value.kind
131
+ : 'procedimiento';
132
+ }
133
+ };
134
+
135
+ /** "un número", "una lista"… para mensajes. */
136
+ export const describeType = (type: TypeName): string => {
137
+ switch (type) {
138
+ case 'lista':
139
+ return 'una lista';
140
+ case 'nulo':
141
+ return '`nulo`';
142
+ default:
143
+ return `un ${type}`;
144
+ }
145
+ };
146
+
147
+ export const isList = (value: Value): value is ListValue =>
148
+ typeof value === 'object' && value !== null && value.kind === 'lista';
149
+
150
+ export const isDomain = (value: Value): value is DomainValue =>
151
+ typeof value === 'object' && value !== null && value.kind === 'dominio';
152
+
153
+ export const isDict = (value: Value): value is DictValue =>
154
+ typeof value === 'object' && value !== null && value.kind === 'diccionario';
155
+
156
+ export const isCallable = (value: Value): value is ProcedureValue | NativeValue =>
157
+ typeof value === 'object' && value !== null && (value.kind === 'procedimiento' || value.kind === 'nativo');
158
+
159
+ const formatNumber = (value: number): string => {
160
+ if (Object.is(value, -0)) return '0';
161
+ if (value === Infinity) return 'infinito';
162
+ if (value === -Infinity) return '-infinito';
163
+ return String(value);
164
+ };
165
+
166
+ /** Representación como se escribiría en código: los textos van entre comillas. */
167
+ export const repr = (value: Value, seen: Set<ListValue | DictValue> = new Set()): string => {
168
+ if (value === null) return 'nulo';
169
+ switch (typeof value) {
170
+ case 'number':
171
+ return formatNumber(value);
172
+ case 'string':
173
+ return JSON.stringify(value);
174
+ case 'boolean':
175
+ return value ? 'verdadero' : 'falso';
176
+ }
177
+ if (value.kind === 'dominio') return `dominio ${value.name}`;
178
+ if (value.kind === 'lista' || value.kind === 'diccionario') {
179
+ if (seen.has(value)) return value.kind === 'lista' ? '[…]' : '{…}';
180
+ seen.add(value);
181
+ const inner =
182
+ value.kind === 'lista'
183
+ ? value.items.map(item => repr(item, seen))
184
+ : [...value.entries].map(([key, item]) => `${repr(key)}: ${repr(item, seen)}`);
185
+ seen.delete(value);
186
+ return value.kind === 'lista' ? `[${inner.join(', ')}]` : `{${inner.join(', ')}}`;
187
+ }
188
+ return value.name ? `procedimiento ${value.name}` : 'procedimiento';
189
+ };
190
+
191
+ /** Como lo muestra `imprimir`: los textos sin comillas. */
192
+ export const show = (value: Value): string => (typeof value === 'string' ? value : repr(value));
193
+
194
+ type Container = ListValue | DictValue;
195
+
196
+ /**
197
+ * Igualdad por valor: dos listas son iguales si tienen los mismos elementos en el mismo orden, y dos
198
+ * diccionarios si tienen las mismas claves con los mismos valores, en cualquier orden.
199
+ */
200
+ export const equals = (a: Value, b: Value, seen: Array<[Container, Container]> = []): boolean => {
201
+ if (a === b) return true;
202
+ const bothLists = isList(a) && isList(b);
203
+ const bothDicts = isDict(a) && isDict(b);
204
+ if (!bothLists && !bothDicts) return false;
205
+ if (seen.some(([x, y]) => x === a && y === b)) return true;
206
+ seen.push([a as Container, b as Container]);
207
+ let result: boolean;
208
+ if (bothLists) {
209
+ const [x, y] = [a as ListValue, b as ListValue];
210
+ result = x.items.length === y.items.length && x.items.every((item, index) => equals(item, y.items[index]!, seen));
211
+ } else {
212
+ const [x, y] = [a as DictValue, b as DictValue];
213
+ result =
214
+ x.entries.size === y.entries.size &&
215
+ [...x.entries].every(([key, item]) => y.entries.has(key) && equals(item, y.entries.get(key)!, seen));
216
+ }
217
+ seen.pop();
218
+ return result;
219
+ };
220
+
221
+ /**
222
+ * Un alcance: las variables de un bloque. `parent` apunta al alcance que lo contiene.
223
+ */
224
+ export class Scope {
225
+ readonly variables = new Map<string, Value>();
226
+ /** Nombres de este alcance declarados con `constante`. */
227
+ readonly constants = new Set<string>();
228
+ /** Variables de `para … desde … hasta` en este alcance: avanzan solas. */
229
+ readonly loopVariables = new Set<string>();
230
+
231
+ constructor(readonly parent?: Scope) {}
232
+
233
+ lookup(name: string): Scope | undefined {
234
+ let scope: Scope | undefined = this;
235
+ while (scope) {
236
+ if (scope.variables.has(name)) return scope;
237
+ scope = scope.parent;
238
+ }
239
+ return undefined;
240
+ }
241
+ }