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.
- package/CHANGELOG.md +15 -0
- package/README.md +159 -2
- package/dist/index.js +154 -0
- package/dist/index.js.map +30 -0
- package/dist/worker.js +153 -0
- package/dist/worker.js.map +25 -0
- package/docs/README.md +27 -0
- package/docs/arquitectura.md +174 -0
- package/docs/especificacion-v1.md +701 -0
- package/docs/guia-aprender-desde-el-codigo.md +91 -0
- package/docs/integracion.md +104 -0
- package/docs/lenguaje.md +537 -0
- package/docs/modo-mundo.md +128 -0
- package/docs/recetario.md +159 -0
- package/docs/repl.md +64 -0
- package/package.json +66 -4
- package/src/catalog.ts +256 -0
- package/src/challenge.ts +138 -0
- package/src/engine.ts +104 -0
- package/src/error-codes.ts +427 -0
- package/src/errors.ts +56 -0
- package/src/flow.ts +385 -0
- package/src/index.ts +81 -0
- package/src/printer.ts +133 -0
- package/src/runtime/builtins.ts +1426 -0
- package/src/runtime/compiler.ts +499 -0
- package/src/runtime/costs.ts +96 -0
- package/src/runtime/machine.ts +1521 -0
- package/src/runtime/prelude.ts +230 -0
- package/src/runtime/suggest.ts +33 -0
- package/src/runtime/values.ts +241 -0
- package/src/sandbox-host.ts +310 -0
- package/src/sandbox.ts +95 -0
- package/src/syntax/ast.ts +344 -0
- package/src/syntax/lexer.ts +266 -0
- package/src/syntax/parser.ts +1090 -0
- package/src/syntax/token.ts +215 -0
- package/src/worker.ts +18 -0
- package/src/world.ts +518 -0
|
@@ -0,0 +1,1521 @@
|
|
|
1
|
+
import { CodexivoError } from '../errors';
|
|
2
|
+
import type { ErrorInfo } from '../errors';
|
|
3
|
+
import type * as ast from '../syntax/ast';
|
|
4
|
+
import type { Span } from '../syntax/token';
|
|
5
|
+
import type { RobotEvent, World, WorldState } from '../world';
|
|
6
|
+
import { constants as libraryConstants, internals, natives as defaultNatives } from './builtins';
|
|
7
|
+
import { COSTS } from './costs';
|
|
8
|
+
import { libraryProcedures } from './prelude';
|
|
9
|
+
import type { Chunk, DecisionLabel, Instruction } from './compiler';
|
|
10
|
+
import {
|
|
11
|
+
describeType,
|
|
12
|
+
equals,
|
|
13
|
+
isCallable,
|
|
14
|
+
isDict,
|
|
15
|
+
isDomain,
|
|
16
|
+
isList,
|
|
17
|
+
repr,
|
|
18
|
+
Scope,
|
|
19
|
+
typeOf,
|
|
20
|
+
WAIT_FOR_INPUT,
|
|
21
|
+
} from './values';
|
|
22
|
+
import type {
|
|
23
|
+
DictKey,
|
|
24
|
+
DictValue,
|
|
25
|
+
DomainValue,
|
|
26
|
+
ListValue,
|
|
27
|
+
NativeContext,
|
|
28
|
+
NativeValue,
|
|
29
|
+
ProcedureValue,
|
|
30
|
+
Value,
|
|
31
|
+
} from './values';
|
|
32
|
+
import { suggestName } from './suggest';
|
|
33
|
+
|
|
34
|
+
export type MachineStatus = 'lista' | 'pausada' | 'esperando_entrada' | 'terminada' | 'error';
|
|
35
|
+
|
|
36
|
+
export interface MachineOptions {
|
|
37
|
+
/** Máximo de operaciones antes de detener el programa. Por defecto 10 000 000. */
|
|
38
|
+
maxOperations?: number;
|
|
39
|
+
/** Máximo de llamadas anidadas. Por defecto 10 000. */
|
|
40
|
+
maxCallDepth?: number;
|
|
41
|
+
/** Máximo de caracteres de salida. Por defecto 1 000 000. */
|
|
42
|
+
maxOutput?: number;
|
|
43
|
+
/** Líneas de entrada para `leer()`. */
|
|
44
|
+
input?: string | string[];
|
|
45
|
+
/** Si es `true`, `leer()` sin entrada pausa la máquina en lugar de regresar `nulo`. */
|
|
46
|
+
interactiveInput?: boolean;
|
|
47
|
+
/** Procedimientos nativos adicionales. */
|
|
48
|
+
natives?: Record<string, NativeValue>;
|
|
49
|
+
/** Mundo del robot (de `createWorld`): agrega sus procedimientos y su estado a `snapshot()`. */
|
|
50
|
+
world?: World;
|
|
51
|
+
/** Líneas donde `run()` se detiene antes de ejecutar la sentencia. */
|
|
52
|
+
breakpoints?: number[];
|
|
53
|
+
/** Alcance global compartido entre ejecuciones (lo usa el REPL). */
|
|
54
|
+
globals?: Scope;
|
|
55
|
+
/** Permite volver a declarar una variable existente (útil en el REPL). */
|
|
56
|
+
allowRedeclare?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Semilla de `aleatorio`. Con la misma semilla (y la misma entrada) la ejecución se repite idéntica. Si no
|
|
59
|
+
* se da, la máquina elige una y la expone en `machine.seed`.
|
|
60
|
+
*/
|
|
61
|
+
seed?: number;
|
|
62
|
+
/** Recibe cada evento de la ejecución. Activarlo hace la ejecución más lenta. */
|
|
63
|
+
onEvent?: (event: MachineEvent) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── eventos y fotografías ────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export type SerializedValue =
|
|
69
|
+
| { type: 'número'; value: number; repr: string }
|
|
70
|
+
| { type: 'texto'; value: string; repr: string }
|
|
71
|
+
| { type: 'lógico'; value: boolean; repr: string }
|
|
72
|
+
| { type: 'nulo'; value: null; repr: string }
|
|
73
|
+
| { type: 'lista'; ref: number; length: number; repr: string }
|
|
74
|
+
| { type: 'diccionario'; ref: number; size: number; repr: string }
|
|
75
|
+
| { type: 'dominio'; ref: number; name: string; repr: string }
|
|
76
|
+
| { type: 'procedimiento'; name?: string; repr: string };
|
|
77
|
+
|
|
78
|
+
/** Contenido de una lista o de un diccionario en `Snapshot.heap`. */
|
|
79
|
+
export type HeapEntry =
|
|
80
|
+
| SerializedValue[]
|
|
81
|
+
| { entries: Array<{ key: SerializedValue; value: SerializedValue }> }
|
|
82
|
+
/** Un dominio: sus miembros en orden de declaración, y cuáles se pueden cambiar desde fuera. */
|
|
83
|
+
| { domain: string; members: Array<{ name: string; value: SerializedValue; writable: boolean }> };
|
|
84
|
+
|
|
85
|
+
export const serialize = (value: Value): SerializedValue => {
|
|
86
|
+
const text = repr(value);
|
|
87
|
+
if (value === null) return { type: 'nulo', value: null, repr: text };
|
|
88
|
+
switch (typeof value) {
|
|
89
|
+
case 'number':
|
|
90
|
+
return { type: 'número', value, repr: text };
|
|
91
|
+
case 'string':
|
|
92
|
+
return { type: 'texto', value, repr: text };
|
|
93
|
+
case 'boolean':
|
|
94
|
+
return { type: 'lógico', value, repr: text };
|
|
95
|
+
}
|
|
96
|
+
if (value.kind === 'lista') return { type: 'lista', ref: value.id, length: value.items.length, repr: text };
|
|
97
|
+
if (value.kind === 'diccionario') return { type: 'diccionario', ref: value.id, size: value.entries.size, repr: text };
|
|
98
|
+
if (value.kind === 'dominio') return { type: 'dominio', ref: value.id, name: value.name, repr: text };
|
|
99
|
+
return { type: 'procedimiento', name: value.name, repr: text };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
interface EventBase {
|
|
103
|
+
/** Operaciones ejecutadas hasta este evento. */
|
|
104
|
+
operations: number;
|
|
105
|
+
/** Profundidad de llamadas: 1 en el programa principal. */
|
|
106
|
+
depth: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type MachineEvent = EventBase &
|
|
110
|
+
(
|
|
111
|
+
| { type: 'paso'; nodeId: number; nodeKind: ast.Statement['kind']; span: Span }
|
|
112
|
+
| { type: 'declara'; name: string; constant: boolean; value: SerializedValue; nodeId: number; span: Span }
|
|
113
|
+
| { type: 'asigna'; name: string; before: SerializedValue; after: SerializedValue; nodeId: number; span: Span }
|
|
114
|
+
/** Cambio de un elemento de una lista (`lista[i] = …`); `ref` identifica la lista. */
|
|
115
|
+
| {
|
|
116
|
+
type: 'modifica';
|
|
117
|
+
ref: number;
|
|
118
|
+
key: SerializedValue;
|
|
119
|
+
before: SerializedValue;
|
|
120
|
+
after: SerializedValue;
|
|
121
|
+
nodeId: number;
|
|
122
|
+
span: Span;
|
|
123
|
+
}
|
|
124
|
+
| { type: 'decide'; label: DecisionLabel; value: boolean; nodeId: number; ownerId: number; span: Span }
|
|
125
|
+
| {
|
|
126
|
+
type: 'opera';
|
|
127
|
+
operator: string;
|
|
128
|
+
operands: SerializedValue[];
|
|
129
|
+
result: SerializedValue;
|
|
130
|
+
nodeId: number;
|
|
131
|
+
span: Span;
|
|
132
|
+
}
|
|
133
|
+
| { type: 'llama'; name: string; args: SerializedValue[]; nodeId: number; span: Span }
|
|
134
|
+
| { type: 'regresa'; name: string; value: SerializedValue }
|
|
135
|
+
| (RobotEvent & { nodeId: number; span: Span })
|
|
136
|
+
| { type: 'imprime'; text: string }
|
|
137
|
+
| { type: 'lee'; text: string | null }
|
|
138
|
+
| { type: 'error'; error: ErrorInfo }
|
|
139
|
+
| { type: 'fin'; result: SerializedValue }
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
export interface FrameSnapshot {
|
|
143
|
+
name: string;
|
|
144
|
+
callSpan?: Span;
|
|
145
|
+
/** `true` en un procedimiento de la biblioteca escrito en Codexivo; sus variables no se muestran. */
|
|
146
|
+
library?: boolean;
|
|
147
|
+
/** Alcances del procedimiento, del más externo al más interno. */
|
|
148
|
+
scopes: Array<Record<string, SerializedValue>>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface Snapshot {
|
|
152
|
+
status: MachineStatus;
|
|
153
|
+
operations: number;
|
|
154
|
+
/** Puntos de pausa por los que ya pasó la ejecución (ver `Machine.steps`). */
|
|
155
|
+
steps: number;
|
|
156
|
+
/** Nodo donde está detenida la máquina. */
|
|
157
|
+
nodeId?: number;
|
|
158
|
+
span?: Span;
|
|
159
|
+
/** Llamadas activas, del programa principal a la más reciente. */
|
|
160
|
+
frames: FrameSnapshot[];
|
|
161
|
+
/** Contenido de las listas que aparecen en `frames`, por `ref`. */
|
|
162
|
+
heap: Record<number, HeapEntry>;
|
|
163
|
+
output: string;
|
|
164
|
+
/** Estado del mundo, si la máquina corre en modo mundo. */
|
|
165
|
+
world?: WorldState;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── máquina ──────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
interface Frame {
|
|
171
|
+
chunk: Chunk;
|
|
172
|
+
pc: number;
|
|
173
|
+
scope: Scope;
|
|
174
|
+
/** Primer alcance del marco; el que contiene a los parámetros. */
|
|
175
|
+
root: Scope;
|
|
176
|
+
/** Posición en la pila de valores donde empieza este marco (el procedimiento llamado está justo antes). */
|
|
177
|
+
stackBase: number;
|
|
178
|
+
name: string;
|
|
179
|
+
callNode?: ast.Call;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const DEFAULTS = {
|
|
183
|
+
maxOperations: 10_000_000,
|
|
184
|
+
maxCallDepth: 10_000,
|
|
185
|
+
maxOutput: 1_000_000,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const VISIBLE_FROM_LIBRARY = new Set<MachineEvent['type']>(['llama', 'regresa', 'imprime', 'lee', 'error', 'fin']);
|
|
189
|
+
|
|
190
|
+
const NO_COUNT = new Set<Instruction['op']>([
|
|
191
|
+
'step',
|
|
192
|
+
'enterScope',
|
|
193
|
+
'exitScope',
|
|
194
|
+
'jump',
|
|
195
|
+
'dup',
|
|
196
|
+
'pop',
|
|
197
|
+
'repeatStart',
|
|
198
|
+
'forEachStart',
|
|
199
|
+
'rangeStart',
|
|
200
|
+
'halt',
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Ejecuta un programa compilado. Tiene su propia pila de llamadas, así que la recursión no depende
|
|
205
|
+
* de la pila de JavaScript, y se puede pausar en cualquier sentencia o decisión.
|
|
206
|
+
*/
|
|
207
|
+
export class Machine {
|
|
208
|
+
private readonly frames: Frame[] = [];
|
|
209
|
+
private readonly stack: Value[] = [];
|
|
210
|
+
private readonly natives: Record<string, NativeValue>;
|
|
211
|
+
private readonly inputQueue: string[];
|
|
212
|
+
private breakpoints: Set<number>;
|
|
213
|
+
private readonly limits: typeof DEFAULTS;
|
|
214
|
+
private readonly onEvent?: (event: MachineEvent) => void;
|
|
215
|
+
private readonly outputParts: string[] = [];
|
|
216
|
+
private outputLength = 0;
|
|
217
|
+
private nextId = 1;
|
|
218
|
+
private lastValue: Value = null;
|
|
219
|
+
private inputClosed = false;
|
|
220
|
+
private currentNode?: ast.Node;
|
|
221
|
+
|
|
222
|
+
/** Semilla de `aleatorio` de esta ejecución (la de las opciones, o una elegida al crear la máquina). */
|
|
223
|
+
readonly seed: number;
|
|
224
|
+
private randomState: number;
|
|
225
|
+
|
|
226
|
+
status: MachineStatus = 'lista';
|
|
227
|
+
operations = 0;
|
|
228
|
+
/**
|
|
229
|
+
* Puntos de pausa por los que ya pasó la ejecución: cada `step()` avanza exactamente uno. Como la máquina es
|
|
230
|
+
* determinista, «volver al paso n» es ejecutar otra vez desde el inicio hasta que `steps` llegue a n.
|
|
231
|
+
*/
|
|
232
|
+
steps = 0;
|
|
233
|
+
/** Por qué se detuvo la última vez que quedó `pausada`. */
|
|
234
|
+
pauseReason?: 'paso' | 'presupuesto' | 'punto_de_interrupción';
|
|
235
|
+
result: Value = null;
|
|
236
|
+
error?: CodexivoError;
|
|
237
|
+
|
|
238
|
+
constructor(
|
|
239
|
+
program: Chunk,
|
|
240
|
+
private readonly options: MachineOptions = {},
|
|
241
|
+
) {
|
|
242
|
+
this.natives = { ...defaultNatives, ...options.world?.natives, ...options.natives };
|
|
243
|
+
this.limits = {
|
|
244
|
+
maxOperations: options.maxOperations ?? DEFAULTS.maxOperations,
|
|
245
|
+
maxCallDepth: options.maxCallDepth ?? DEFAULTS.maxCallDepth,
|
|
246
|
+
maxOutput: options.maxOutput ?? DEFAULTS.maxOutput,
|
|
247
|
+
};
|
|
248
|
+
this.inputQueue = typeof options.input === 'string' ? splitLines(options.input) : [...(options.input ?? [])];
|
|
249
|
+
this.breakpoints = new Set(options.breakpoints ?? []);
|
|
250
|
+
this.onEvent = options.onEvent;
|
|
251
|
+
// Elegir la semilla es lo único que no es determinista; desde ahí, todo se puede repetir.
|
|
252
|
+
this.seed = (options.seed ?? Math.floor(Math.random() * 2 ** 32)) >>> 0;
|
|
253
|
+
this.randomState = this.seed;
|
|
254
|
+
const globals = options.globals ?? new Scope();
|
|
255
|
+
this.frames.push({ chunk: program, pc: 0, scope: globals, root: globals, stackBase: 0, name: 'programa' });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Todo lo que el programa ha impreso. */
|
|
259
|
+
get output(): string {
|
|
260
|
+
return this.outputParts.join('');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
get done(): boolean {
|
|
264
|
+
return this.status === 'terminada' || this.status === 'error';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Ejecuta hasta terminar, fallar, esperar entrada, llegar a un punto de interrupción o agotar `budget` operaciones. */
|
|
268
|
+
run(budget = Infinity): MachineStatus {
|
|
269
|
+
return this.execute('run', budget);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Ejecuta hasta la siguiente sentencia o decisión. */
|
|
273
|
+
step(): MachineStatus {
|
|
274
|
+
return this.execute('step', Infinity);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Cambia las líneas donde `run()` se detiene. */
|
|
278
|
+
setBreakpoints(lines: number[]): void {
|
|
279
|
+
this.breakpoints = new Set(lines);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Entrega una línea a un `leer()` que está esperando. */
|
|
283
|
+
provideInput(line: string): void {
|
|
284
|
+
this.inputQueue.push(...splitLines(line));
|
|
285
|
+
if (this.status === 'esperando_entrada') this.status = 'pausada';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Indica que no habrá más entrada: desde ahora `leer()` regresa `nulo` en lugar de esperar. */
|
|
289
|
+
closeInput(): void {
|
|
290
|
+
this.inputClosed = true;
|
|
291
|
+
if (this.status === 'esperando_entrada') this.status = 'pausada';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
snapshot(): Snapshot {
|
|
295
|
+
const heap: Record<number, HeapEntry> = {};
|
|
296
|
+
const collect = (value: Value) => {
|
|
297
|
+
if ((!isList(value) && !isDict(value) && !isDomain(value)) || heap[value.id]) return;
|
|
298
|
+
if (isDomain(value)) {
|
|
299
|
+
heap[value.id] = {
|
|
300
|
+
domain: value.name,
|
|
301
|
+
members: [...value.scope.variables].map(([name, member]) => ({
|
|
302
|
+
name,
|
|
303
|
+
value: serialize(member),
|
|
304
|
+
writable: value.writable.has(name),
|
|
305
|
+
})),
|
|
306
|
+
};
|
|
307
|
+
value.scope.variables.forEach(collect);
|
|
308
|
+
} else if (isList(value)) {
|
|
309
|
+
heap[value.id] = value.items.map(serialize);
|
|
310
|
+
value.items.forEach(collect);
|
|
311
|
+
} else {
|
|
312
|
+
heap[value.id] = {
|
|
313
|
+
entries: [...value.entries].map(([key, item]) => ({ key: serialize(key), value: serialize(item) })),
|
|
314
|
+
};
|
|
315
|
+
value.entries.forEach(collect);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
const frames = this.frames.map(frame => {
|
|
319
|
+
const scopes: Array<Record<string, SerializedValue>> = [];
|
|
320
|
+
let scope: Scope | undefined = frame.scope;
|
|
321
|
+
while (scope) {
|
|
322
|
+
const variables: Record<string, SerializedValue> = {};
|
|
323
|
+
for (const [name, value] of scope.variables) {
|
|
324
|
+
variables[name] = serialize(value);
|
|
325
|
+
collect(value);
|
|
326
|
+
}
|
|
327
|
+
scopes.unshift(variables);
|
|
328
|
+
if (scope === frame.root) break;
|
|
329
|
+
scope = scope.parent;
|
|
330
|
+
}
|
|
331
|
+
if (frame.chunk.hidden) return { name: frame.name, callSpan: frame.callNode?.span, library: true, scopes: [] };
|
|
332
|
+
return { name: frame.name, callSpan: frame.callNode?.span, scopes };
|
|
333
|
+
});
|
|
334
|
+
return {
|
|
335
|
+
status: this.status,
|
|
336
|
+
operations: this.operations,
|
|
337
|
+
steps: this.steps,
|
|
338
|
+
nodeId: this.currentNode?.id,
|
|
339
|
+
span: this.currentNode?.span,
|
|
340
|
+
frames,
|
|
341
|
+
heap,
|
|
342
|
+
output: this.output,
|
|
343
|
+
...(this.options.world ? { world: this.options.world.state() } : {}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── ciclo principal ────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
private execute(mode: 'run' | 'step', budget: number): MachineStatus {
|
|
350
|
+
if (this.done || this.status === 'esperando_entrada') return this.status;
|
|
351
|
+
const stopAt = this.operations + budget;
|
|
352
|
+
let executed = 0;
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
while (true) {
|
|
356
|
+
const frame = this.frames[this.frames.length - 1]!;
|
|
357
|
+
const instruction = frame.chunk.code[frame.pc]!;
|
|
358
|
+
|
|
359
|
+
if (!NO_COUNT.has(instruction.op) && !frame.chunk.hidden) {
|
|
360
|
+
if (this.operations >= this.limits.maxOperations) {
|
|
361
|
+
throw this.limitError(
|
|
362
|
+
'DEMASIADAS_OPERACIONES',
|
|
363
|
+
`El programa superó el límite de ${formatCount(this.limits.maxOperations)} operaciones.`,
|
|
364
|
+
'¿Hay un bucle que nunca termina? Revisa que su condición cambie en algún momento.',
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (this.operations >= stopAt) {
|
|
368
|
+
this.status = 'pausada';
|
|
369
|
+
this.pauseReason = 'presupuesto';
|
|
370
|
+
return this.status;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Los puntos de interrupción detienen `run()` antes de la sentencia.
|
|
375
|
+
if (
|
|
376
|
+
instruction.op === 'step' &&
|
|
377
|
+
mode === 'run' &&
|
|
378
|
+
!frame.chunk.hidden &&
|
|
379
|
+
executed > 0 &&
|
|
380
|
+
this.breakpoints.has(instruction.node.span.start.line)
|
|
381
|
+
) {
|
|
382
|
+
this.status = 'pausada';
|
|
383
|
+
this.pauseReason = 'punto_de_interrupción';
|
|
384
|
+
return this.status;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const pause = this.exec(frame, instruction);
|
|
388
|
+
executed += 1;
|
|
389
|
+
// `exec` puede cambiar el estado.
|
|
390
|
+
const status = this.status as MachineStatus;
|
|
391
|
+
if (status === 'esperando_entrada' || status === 'terminada') return status;
|
|
392
|
+
// El código de la biblioteca nunca es un punto de pausa; sí lo es el de `f` que llama.
|
|
393
|
+
if (pause && !frame.chunk.hidden) {
|
|
394
|
+
this.steps += 1;
|
|
395
|
+
if (mode === 'step') {
|
|
396
|
+
this.status = 'pausada';
|
|
397
|
+
this.pauseReason = 'paso';
|
|
398
|
+
return this.status;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (!(error instanceof CodexivoError)) throw error;
|
|
404
|
+
this.error = error;
|
|
405
|
+
this.status = 'error';
|
|
406
|
+
this.emit({ type: 'error', error: error.toJSON() });
|
|
407
|
+
return this.status;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Ejecuta una instrucción. Regresa `true` si es un punto de pausa para `step()`. */
|
|
412
|
+
private exec(frame: Frame, instruction: Instruction): boolean {
|
|
413
|
+
frame.pc += 1;
|
|
414
|
+
if (!NO_COUNT.has(instruction.op) && !frame.chunk.hidden) this.operations += 1;
|
|
415
|
+
|
|
416
|
+
switch (instruction.op) {
|
|
417
|
+
case 'step':
|
|
418
|
+
if (frame.chunk.hidden) return false;
|
|
419
|
+
this.currentNode = instruction.node;
|
|
420
|
+
this.emit({
|
|
421
|
+
type: 'paso',
|
|
422
|
+
nodeId: instruction.node.id,
|
|
423
|
+
nodeKind: instruction.node.kind,
|
|
424
|
+
span: instruction.node.span,
|
|
425
|
+
});
|
|
426
|
+
return true;
|
|
427
|
+
|
|
428
|
+
case 'const':
|
|
429
|
+
this.stack.push(instruction.value);
|
|
430
|
+
return false;
|
|
431
|
+
|
|
432
|
+
case 'list': {
|
|
433
|
+
const items = this.stack.splice(this.stack.length - instruction.count, instruction.count);
|
|
434
|
+
this.stack.push(this.newList(items));
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
case 'dict': {
|
|
439
|
+
const values = this.stack.splice(this.stack.length - instruction.count * 2, instruction.count * 2);
|
|
440
|
+
const entries = new Map<DictKey, Value>();
|
|
441
|
+
instruction.node.entries.forEach((entry, index) => {
|
|
442
|
+
entries.set(this.dictKey(values[index * 2]!, entry.key), values[index * 2 + 1]!);
|
|
443
|
+
});
|
|
444
|
+
this.stack.push(this.newDict(entries));
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
case 'load': {
|
|
449
|
+
const scope = frame.scope.lookup(instruction.name);
|
|
450
|
+
if (scope) {
|
|
451
|
+
this.stack.push(scope.variables.get(instruction.name)!);
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
const native =
|
|
455
|
+
(frame.chunk.hidden ? internals[instruction.name] : undefined) ??
|
|
456
|
+
this.natives[instruction.name] ??
|
|
457
|
+
libraryProcedures[instruction.name] ??
|
|
458
|
+
libraryConstants[instruction.name];
|
|
459
|
+
if (native !== undefined && !instruction.forUpdate) {
|
|
460
|
+
this.stack.push(native);
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
if (instruction.forUpdate) throw this.undeclaredAssignment(instruction.name, instruction.node);
|
|
464
|
+
throw this.unknownName(frame, instruction.name, instruction.node);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
case 'declare': {
|
|
468
|
+
const value = this.stack.pop()!;
|
|
469
|
+
if (frame.scope.variables.has(instruction.name) && !this.options.allowRedeclare) {
|
|
470
|
+
throw this.runtimeError(
|
|
471
|
+
'VARIABLE_REPETIDA',
|
|
472
|
+
`La variable \`${instruction.name}\` ya existe en este bloque.`,
|
|
473
|
+
instruction.target,
|
|
474
|
+
`Para cambiar su valor escribe \`${instruction.name} = …\`, sin \`variable\`.`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
frame.scope.variables.set(instruction.name, value);
|
|
478
|
+
if (instruction.constant) frame.scope.constants.add(instruction.name);
|
|
479
|
+
else frame.scope.constants.delete(instruction.name);
|
|
480
|
+
if (instruction.loop) frame.scope.loopVariables.add(instruction.name);
|
|
481
|
+
else frame.scope.loopVariables.delete(instruction.name);
|
|
482
|
+
this.emit({
|
|
483
|
+
type: 'declara',
|
|
484
|
+
name: instruction.name,
|
|
485
|
+
constant: instruction.constant,
|
|
486
|
+
value: serialize(value),
|
|
487
|
+
nodeId: instruction.node.id,
|
|
488
|
+
span: instruction.node.span,
|
|
489
|
+
});
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
case 'store': {
|
|
494
|
+
const value = this.stack.pop()!;
|
|
495
|
+
const scope = frame.scope.lookup(instruction.name);
|
|
496
|
+
if (!scope) throw this.undeclaredAssignment(instruction.name, instruction.target);
|
|
497
|
+
if (scope.loopVariables.has(instruction.name)) {
|
|
498
|
+
throw this.runtimeError(
|
|
499
|
+
'VARIABLE_DE_PARA',
|
|
500
|
+
`La variable de un \`para\` avanza sola; \`${instruction.name}\` no se puede cambiar dentro del bucle.`,
|
|
501
|
+
instruction.node,
|
|
502
|
+
`Si necesitas otro valor, guárdalo en una variable nueva: \`variable otro = ${instruction.name} + 1\`.`,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
if (scope.constants.has(instruction.name)) {
|
|
506
|
+
if (isDomain(scope.variables.get(instruction.name)!)) {
|
|
507
|
+
throw this.runtimeError(
|
|
508
|
+
'CONSTANTE_REASIGNADA',
|
|
509
|
+
`\`${instruction.name}\` es un dominio; su nombre no se puede usar para otra cosa.`,
|
|
510
|
+
instruction.node,
|
|
511
|
+
`Para cambiar un miembro escribe \`${instruction.name}.miembro = …\`; para otro valor, usa otro nombre.`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
throw this.constantReassignment(instruction.name, instruction.node);
|
|
515
|
+
}
|
|
516
|
+
const before = scope.variables.get(instruction.name)!;
|
|
517
|
+
scope.variables.set(instruction.name, value);
|
|
518
|
+
if (this.onEvent) {
|
|
519
|
+
this.emit({
|
|
520
|
+
type: 'asigna',
|
|
521
|
+
name: instruction.name,
|
|
522
|
+
before: serialize(before),
|
|
523
|
+
after: serialize(value),
|
|
524
|
+
nodeId: instruction.node.id,
|
|
525
|
+
span: instruction.node.span,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
case 'setIndex': {
|
|
532
|
+
const value = this.stack.pop()!;
|
|
533
|
+
const index = this.stack.pop()!;
|
|
534
|
+
const object = this.stack.pop()!;
|
|
535
|
+
this.setIndex(object, index, value, instruction.target, instruction.node);
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
case 'dup':
|
|
540
|
+
this.stack.push(...this.stack.slice(this.stack.length - instruction.count));
|
|
541
|
+
return false;
|
|
542
|
+
|
|
543
|
+
case 'pop':
|
|
544
|
+
this.stack.length -= instruction.count;
|
|
545
|
+
return false;
|
|
546
|
+
|
|
547
|
+
case 'result': {
|
|
548
|
+
const value = this.stack.pop()!;
|
|
549
|
+
if (this.frames.length === 1) this.lastValue = value;
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
case 'unary': {
|
|
554
|
+
const operand = this.stack.pop()!;
|
|
555
|
+
const result = this.unary(instruction.operator, operand, instruction.node);
|
|
556
|
+
this.stack.push(result);
|
|
557
|
+
if (this.onEvent) this.emitOperation(instruction.operator, [operand], result, instruction.node);
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
case 'binary': {
|
|
562
|
+
const right = this.stack.pop()!;
|
|
563
|
+
const left = this.stack.pop()!;
|
|
564
|
+
const result = this.binary(instruction.operator, left, right, instruction.node);
|
|
565
|
+
this.stack.push(result);
|
|
566
|
+
if (this.onEvent) this.emitOperation(instruction.operator, [left, right], result, instruction.node);
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
case 'index': {
|
|
571
|
+
const index = this.stack.pop()!;
|
|
572
|
+
const object = this.stack.pop()!;
|
|
573
|
+
this.stack.push(this.index(object, index, instruction.node));
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
case 'closure': {
|
|
578
|
+
const procedure: ProcedureValue = {
|
|
579
|
+
kind: 'procedimiento',
|
|
580
|
+
id: this.nextId++,
|
|
581
|
+
name: instruction.node.name,
|
|
582
|
+
chunk: instruction.chunk,
|
|
583
|
+
closure: frame.scope,
|
|
584
|
+
};
|
|
585
|
+
this.stack.push(procedure);
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
case 'call':
|
|
590
|
+
return this.call(frame, instruction.argc, instruction.node);
|
|
591
|
+
|
|
592
|
+
case 'return': {
|
|
593
|
+
const value = instruction.hasValue ? this.stack.pop()! : null;
|
|
594
|
+
if (this.frames.length === 1) {
|
|
595
|
+
this.finish(value);
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
const caller = this.frames[this.frames.length - 2]!;
|
|
599
|
+
if (!(frame.chunk.hidden && caller.chunk.hidden)) {
|
|
600
|
+
this.emit({ type: 'regresa', name: frame.name, value: serialize(value) });
|
|
601
|
+
}
|
|
602
|
+
this.frames.pop();
|
|
603
|
+
this.stack.length = frame.stackBase - 1;
|
|
604
|
+
this.stack.push(value);
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
case 'jump':
|
|
609
|
+
frame.pc = instruction.target;
|
|
610
|
+
return false;
|
|
611
|
+
|
|
612
|
+
case 'branch': {
|
|
613
|
+
const condition = this.stack.pop()!;
|
|
614
|
+
if (typeof condition !== 'boolean')
|
|
615
|
+
throw this.nonBooleanCondition(instruction.label, condition, instruction.node);
|
|
616
|
+
if (!frame.chunk.hidden) this.currentNode = instruction.node;
|
|
617
|
+
this.emit({
|
|
618
|
+
type: 'decide',
|
|
619
|
+
label: instruction.label,
|
|
620
|
+
value: condition,
|
|
621
|
+
nodeId: instruction.node.id,
|
|
622
|
+
ownerId: instruction.owner.id,
|
|
623
|
+
span: instruction.node.span,
|
|
624
|
+
});
|
|
625
|
+
if (condition === instruction.jumpWhen) frame.pc = instruction.target;
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
case 'repeatStart': {
|
|
630
|
+
const count = this.stack[this.stack.length - 1]!;
|
|
631
|
+
if (typeof count !== 'number' || !Number.isInteger(count) || count < 0) {
|
|
632
|
+
throw this.invalidRepeatCount(count, instruction.node);
|
|
633
|
+
}
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
case 'repeatNext': {
|
|
638
|
+
const remaining = this.stack[this.stack.length - 1] as number;
|
|
639
|
+
const again = remaining > 0;
|
|
640
|
+
if (again) this.stack[this.stack.length - 1] = remaining - 1;
|
|
641
|
+
else this.stack.pop();
|
|
642
|
+
if (!frame.chunk.hidden) this.currentNode = instruction.node;
|
|
643
|
+
this.emit({
|
|
644
|
+
type: 'decide',
|
|
645
|
+
label: 'repetir',
|
|
646
|
+
value: again,
|
|
647
|
+
nodeId: instruction.node.id,
|
|
648
|
+
ownerId: instruction.owner.id,
|
|
649
|
+
span: instruction.node.span,
|
|
650
|
+
});
|
|
651
|
+
if (!again) frame.pc = instruction.target;
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
case 'rangeStart': {
|
|
656
|
+
const step = this.stack.pop()!;
|
|
657
|
+
const to = this.stack.pop()!;
|
|
658
|
+
const from = this.stack.pop()!;
|
|
659
|
+
const { owner } = instruction;
|
|
660
|
+
const checkNumber = (value: Value, node: ast.Node, what: string): number => {
|
|
661
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
662
|
+
throw this.runtimeError(
|
|
663
|
+
'PARA_LÍMITE_INVÁLIDO',
|
|
664
|
+
`${what} de \`para\` tiene que ser un número, pero es ${repr(value)}.`,
|
|
665
|
+
node,
|
|
666
|
+
typeof value === 'string' ? 'Convierte el texto con `número(…)`.' : undefined,
|
|
667
|
+
);
|
|
668
|
+
};
|
|
669
|
+
const start = checkNumber(from, owner.from, 'El inicio');
|
|
670
|
+
const end = checkNumber(to, owner.to, 'El final');
|
|
671
|
+
const increment = checkNumber(step, owner.step ?? owner, 'El paso');
|
|
672
|
+
if (increment === 0) {
|
|
673
|
+
throw this.runtimeError(
|
|
674
|
+
'PARA_PASO_CERO',
|
|
675
|
+
'El paso de un `para` no puede ser 0: nunca llegaría al final.',
|
|
676
|
+
owner.step ?? owner,
|
|
677
|
+
'Usa un paso positivo para contar hacia arriba o uno negativo para contar hacia abajo.',
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if ((end - start) * increment < 0) {
|
|
681
|
+
// Sin magia: no se invierte solo ni hace cero vueltas en silencio.
|
|
682
|
+
throw this.runtimeError(
|
|
683
|
+
'PARA_SENTIDO_CONTRARIO',
|
|
684
|
+
increment > 0
|
|
685
|
+
? `El \`para\` empieza en ${repr(start)}, que es mayor que ${repr(end)}, y cuenta hacia arriba.`
|
|
686
|
+
: `El \`para\` empieza en ${repr(start)}, que es menor que ${repr(end)}, y cuenta hacia abajo.`,
|
|
687
|
+
instruction.explicitStep ? owner.step! : owner.to,
|
|
688
|
+
instruction.explicitStep
|
|
689
|
+
? `Para ir de ${repr(start)} a ${repr(end)} el paso tiene que ser ${increment > 0 ? 'negativo' : 'positivo'}.`
|
|
690
|
+
: 'Para contar hacia atrás escribe `con_paso -1`.',
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
// Se tolera un error de redondeo pequeño para que `desde 0 hasta 1 con_paso 0.1` incluya el 1.
|
|
694
|
+
const count = Math.floor((end - start) / increment + 1e-9) + 1;
|
|
695
|
+
this.stack.push(start, increment, count, 0);
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
case 'rangeNext': {
|
|
700
|
+
const turn = this.stack[this.stack.length - 1] as number;
|
|
701
|
+
const count = this.stack[this.stack.length - 2] as number;
|
|
702
|
+
const again = turn < count;
|
|
703
|
+
if (again) {
|
|
704
|
+
const start = this.stack[this.stack.length - 4] as number;
|
|
705
|
+
const increment = this.stack[this.stack.length - 3] as number;
|
|
706
|
+
this.stack[this.stack.length - 1] = turn + 1;
|
|
707
|
+
// Se calcula desde el inicio (no sumando) y con 15 cifras, así `0.1 * 3` se ve como 0.3.
|
|
708
|
+
this.stack.push(Number((start + turn * increment).toPrecision(15)));
|
|
709
|
+
} else {
|
|
710
|
+
this.stack.length -= 4;
|
|
711
|
+
}
|
|
712
|
+
if (!frame.chunk.hidden) this.currentNode = instruction.node;
|
|
713
|
+
this.emit({
|
|
714
|
+
type: 'decide',
|
|
715
|
+
label: 'para',
|
|
716
|
+
value: again,
|
|
717
|
+
nodeId: instruction.node.id,
|
|
718
|
+
ownerId: instruction.owner.id,
|
|
719
|
+
span: instruction.node.span,
|
|
720
|
+
});
|
|
721
|
+
if (!again) frame.pc = instruction.target;
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
case 'forEachStart': {
|
|
726
|
+
const iterable = this.stack[this.stack.length - 1]!;
|
|
727
|
+
const { owner } = instruction;
|
|
728
|
+
if (isDict(iterable)) {
|
|
729
|
+
// Las claves se copian al empezar; así se nota si el diccionario cambia durante el recorrido.
|
|
730
|
+
const keys: ListValue = { kind: 'lista', id: 0, items: [...iterable.entries.keys()] };
|
|
731
|
+
this.stack.push(keys, 0);
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
if (owner.value) {
|
|
735
|
+
throw this.runtimeError(
|
|
736
|
+
'PARA_CADA_INVÁLIDO',
|
|
737
|
+
`\`para_cada ${owner.variable.name}, ${owner.value.name}\` recorre un diccionario, no ${describeType(typeOf(iterable))}.`,
|
|
738
|
+
instruction.node,
|
|
739
|
+
isList(iterable) || typeof iterable === 'string'
|
|
740
|
+
? `Para una lista o un texto usa una sola variable: \`para_cada ${owner.value.name} en …\`.`
|
|
741
|
+
: undefined,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
if (!isList(iterable) && typeof iterable !== 'string') {
|
|
745
|
+
throw this.runtimeError(
|
|
746
|
+
'PARA_CADA_INVÁLIDO',
|
|
747
|
+
`\`para_cada\` recorre listas, textos y diccionarios, no ${describeType(typeOf(iterable))}.`,
|
|
748
|
+
instruction.node,
|
|
749
|
+
typeof iterable === 'number'
|
|
750
|
+
? `Para contar, recorre un rango: \`rango(1, ${repr(iterable)})\`.`
|
|
751
|
+
: undefined,
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
this.stack.push(isList(iterable) ? iterable.items.length : iterable.length, 0);
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
case 'forEachNext': {
|
|
759
|
+
const position = this.stack[this.stack.length - 1] as number;
|
|
760
|
+
const state = this.stack[this.stack.length - 2] as number | ListValue;
|
|
761
|
+
const iterable = this.stack[this.stack.length - 3] as ListValue | DictValue | string;
|
|
762
|
+
const length = typeof state === 'number' ? state : state.items.length;
|
|
763
|
+
if (isList(iterable) && iterable.items.length !== length) {
|
|
764
|
+
throw this.runtimeError(
|
|
765
|
+
'LISTA_MODIFICADA_AL_RECORRER',
|
|
766
|
+
`La lista cambió de tamaño mientras se recorría: tenía ${length} y ahora tiene ${iterable.items.length}.`,
|
|
767
|
+
instruction.node,
|
|
768
|
+
'Recorre una copia o junta los cambios en otra lista y aplícalos al terminar el `para_cada`.',
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
if (
|
|
772
|
+
isDict(iterable) &&
|
|
773
|
+
(iterable.entries.size !== length ||
|
|
774
|
+
(position < length && !iterable.entries.has((state as ListValue).items[position] as DictKey)))
|
|
775
|
+
) {
|
|
776
|
+
throw this.runtimeError(
|
|
777
|
+
'DICCIONARIO_MODIFICADO_AL_RECORRER',
|
|
778
|
+
'Se agregaron o quitaron claves del diccionario mientras se recorría.',
|
|
779
|
+
instruction.node,
|
|
780
|
+
'Recorre `claves(d)`, que es una lista nueva, o junta los cambios y aplícalos al terminar.',
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
const again = position < length;
|
|
784
|
+
if (again) {
|
|
785
|
+
this.stack[this.stack.length - 1] = position + 1;
|
|
786
|
+
if (isDict(iterable)) {
|
|
787
|
+
const key = (state as ListValue).items[position] as DictKey;
|
|
788
|
+
if (instruction.owner.value) this.stack.push(iterable.entries.get(key)!);
|
|
789
|
+
this.stack.push(key);
|
|
790
|
+
} else {
|
|
791
|
+
this.stack.push(isList(iterable) ? iterable.items[position]! : iterable[position]!);
|
|
792
|
+
}
|
|
793
|
+
} else {
|
|
794
|
+
this.stack.length -= 3;
|
|
795
|
+
}
|
|
796
|
+
if (!frame.chunk.hidden) this.currentNode = instruction.node;
|
|
797
|
+
this.emit({
|
|
798
|
+
type: 'decide',
|
|
799
|
+
label: 'para_cada',
|
|
800
|
+
value: again,
|
|
801
|
+
nodeId: instruction.node.id,
|
|
802
|
+
ownerId: instruction.owner.id,
|
|
803
|
+
span: instruction.node.span,
|
|
804
|
+
});
|
|
805
|
+
if (!again) frame.pc = instruction.target;
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
case 'shortCircuit': {
|
|
810
|
+
const left = this.stack[this.stack.length - 1]!;
|
|
811
|
+
if (typeof left !== 'boolean') throw this.nonBooleanLogical(instruction.node, left, instruction.node.left);
|
|
812
|
+
const decided = instruction.node.operator === 'y' ? left === false : left === true;
|
|
813
|
+
if (decided) {
|
|
814
|
+
frame.pc = instruction.target;
|
|
815
|
+
if (this.onEvent) this.emitOperation(instruction.node.operator, [left], left, instruction.node);
|
|
816
|
+
} else {
|
|
817
|
+
this.stack.pop();
|
|
818
|
+
}
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
case 'checkLogical': {
|
|
823
|
+
const right = this.stack[this.stack.length - 1]!;
|
|
824
|
+
if (typeof right !== 'boolean') throw this.nonBooleanLogical(instruction.node, right, instruction.node.right);
|
|
825
|
+
if (this.onEvent) {
|
|
826
|
+
const left = instruction.node.operator === 'y';
|
|
827
|
+
this.emitOperation(instruction.node.operator, [left, right], right, instruction.node);
|
|
828
|
+
}
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
case 'enterScope':
|
|
833
|
+
case 'domainStart':
|
|
834
|
+
frame.scope = new Scope(frame.scope);
|
|
835
|
+
return false;
|
|
836
|
+
|
|
837
|
+
case 'domainEnd': {
|
|
838
|
+
const domain: DomainValue = {
|
|
839
|
+
kind: 'dominio',
|
|
840
|
+
id: this.nextId++,
|
|
841
|
+
name: instruction.name,
|
|
842
|
+
scope: frame.scope,
|
|
843
|
+
writable: new Set(instruction.writable),
|
|
844
|
+
};
|
|
845
|
+
frame.scope = frame.scope.parent ?? frame.scope;
|
|
846
|
+
if (frame.scope.variables.has(instruction.name) && !this.options.allowRedeclare) {
|
|
847
|
+
throw this.runtimeError(
|
|
848
|
+
'VARIABLE_REPETIDA',
|
|
849
|
+
`Ya existe algo llamado \`${instruction.name}\` en este bloque.`,
|
|
850
|
+
instruction.node.name,
|
|
851
|
+
'Cada dominio necesita un nombre propio en su bloque.',
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
// El nombre de un dominio no se reasigna: siempre se refiere al mismo espacio de nombres.
|
|
855
|
+
frame.scope.variables.set(instruction.name, domain);
|
|
856
|
+
frame.scope.constants.add(instruction.name);
|
|
857
|
+
this.emit({
|
|
858
|
+
type: 'declara',
|
|
859
|
+
name: instruction.name,
|
|
860
|
+
constant: true,
|
|
861
|
+
value: serialize(domain),
|
|
862
|
+
nodeId: instruction.node.id,
|
|
863
|
+
span: instruction.node.span,
|
|
864
|
+
});
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
case 'exitScope':
|
|
869
|
+
frame.scope = frame.scope.parent ?? frame.scope;
|
|
870
|
+
return false;
|
|
871
|
+
|
|
872
|
+
case 'halt':
|
|
873
|
+
this.finish(this.lastValue);
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private finish(value: Value): void {
|
|
879
|
+
this.result = value;
|
|
880
|
+
this.status = 'terminada';
|
|
881
|
+
this.emit({ type: 'fin', result: serialize(value) });
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// ── llamadas ───────────────────────────────────────────────────────────────
|
|
885
|
+
|
|
886
|
+
private call(frame: Frame, argc: number, node: ast.Call): boolean {
|
|
887
|
+
const calleeIndex = this.stack.length - argc - 1;
|
|
888
|
+
const callee = this.stack[calleeIndex]!;
|
|
889
|
+
const name =
|
|
890
|
+
callee !== null && typeof callee === 'object' && 'name' in callee && callee.name
|
|
891
|
+
? callee.name
|
|
892
|
+
: describeCallee(node);
|
|
893
|
+
|
|
894
|
+
if (!isCallable(callee)) {
|
|
895
|
+
throw this.runtimeError(
|
|
896
|
+
'NO_ES_PROCEDIMIENTO',
|
|
897
|
+
`\`${describeCallee(node)}\` no es un procedimiento, es ${describeType(typeOf(callee))}.`,
|
|
898
|
+
node.callee,
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const args = this.stack.slice(calleeIndex + 1);
|
|
903
|
+
const expected =
|
|
904
|
+
callee.kind === 'nativo'
|
|
905
|
+
? callee.arity
|
|
906
|
+
: callee.chunk.minParams !== undefined
|
|
907
|
+
? ([callee.chunk.minParams, callee.chunk.params.length] as [number, number])
|
|
908
|
+
: callee.chunk.params.length;
|
|
909
|
+
checkArity(expected, argc, name, (message, hint) =>
|
|
910
|
+
this.runtimeError(
|
|
911
|
+
'CANTIDAD_DE_ARGUMENTOS',
|
|
912
|
+
message,
|
|
913
|
+
node,
|
|
914
|
+
hint ??
|
|
915
|
+
(frame.chunk.hidden
|
|
916
|
+
? `\`${frame.name}\` llama al procedimiento que recibió con ${argc === 1 ? '1 valor' : `${argc} valores`}.`
|
|
917
|
+
: undefined),
|
|
918
|
+
),
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
if (callee.kind === 'nativo') {
|
|
922
|
+
const result = callee.call(args, this.nativeContext(node));
|
|
923
|
+
if (result === WAIT_FOR_INPUT) {
|
|
924
|
+
// Repetimos la llamada cuando llegue la entrada.
|
|
925
|
+
frame.pc -= 1;
|
|
926
|
+
if (!frame.chunk.hidden) this.operations -= 1;
|
|
927
|
+
this.status = 'esperando_entrada';
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
if (!frame.chunk.hidden) this.charge(callee.cost, args);
|
|
931
|
+
this.stack.length = calleeIndex;
|
|
932
|
+
this.stack.push(result);
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// El primer marco es el programa principal, no una llamada.
|
|
937
|
+
if (this.frames.length - 1 >= this.limits.maxCallDepth) {
|
|
938
|
+
throw this.limitError(
|
|
939
|
+
'DEMASIADAS_LLAMADAS',
|
|
940
|
+
`Hay más de ${formatCount(this.limits.maxCallDepth)} llamadas anidadas.`,
|
|
941
|
+
'Si es recursión, revisa que exista un caso base que deje de llamar al procedimiento.',
|
|
942
|
+
node,
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// La biblioteca cobra según la tabla de costos; dentro de ella, nada más se cobra (§10).
|
|
947
|
+
if (callee.chunk.hidden && !frame.chunk.hidden) this.charge(COSTS[callee.chunk.name], args);
|
|
948
|
+
const scope = new Scope(callee.closure);
|
|
949
|
+
callee.chunk.params.forEach((param, index) => scope.variables.set(param, args[index] ?? null));
|
|
950
|
+
if (!(frame.chunk.hidden && callee.chunk.hidden)) {
|
|
951
|
+
this.emit({ type: 'llama', name, args: args.map(serialize), nodeId: node.id, span: node.span });
|
|
952
|
+
}
|
|
953
|
+
this.frames.push({
|
|
954
|
+
chunk: callee.chunk,
|
|
955
|
+
pc: 0,
|
|
956
|
+
scope,
|
|
957
|
+
root: scope,
|
|
958
|
+
stackBase: calleeIndex + 1,
|
|
959
|
+
name,
|
|
960
|
+
callNode: node,
|
|
961
|
+
});
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** Cobra lo que cuesta un procedimiento de la biblioteca; la instrucción `call` ya contó 1. */
|
|
966
|
+
private charge(cost: ((args: Value[]) => number) | undefined, args: Value[]): void {
|
|
967
|
+
if (cost) this.operations += Math.max(0, cost(args) - 1);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
private nativeContext(node: ast.Call): NativeContext {
|
|
971
|
+
return {
|
|
972
|
+
write: text => {
|
|
973
|
+
this.outputLength += text.length;
|
|
974
|
+
if (this.outputLength > this.limits.maxOutput) {
|
|
975
|
+
throw this.limitError(
|
|
976
|
+
'DEMASIADA_SALIDA',
|
|
977
|
+
`El programa imprimió más de ${formatCount(this.limits.maxOutput)} caracteres.`,
|
|
978
|
+
'¿Hay un `imprimir` dentro de un bucle que nunca termina?',
|
|
979
|
+
node,
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
this.outputParts.push(text);
|
|
983
|
+
this.emit({ type: 'imprime', text });
|
|
984
|
+
},
|
|
985
|
+
readLine: () => {
|
|
986
|
+
if (this.inputQueue.length > 0) {
|
|
987
|
+
const line = this.inputQueue.shift()!;
|
|
988
|
+
this.emit({ type: 'lee', text: line });
|
|
989
|
+
return line;
|
|
990
|
+
}
|
|
991
|
+
if (this.options.interactiveInput && !this.inputClosed) return WAIT_FOR_INPUT;
|
|
992
|
+
this.emit({ type: 'lee', text: null });
|
|
993
|
+
return null;
|
|
994
|
+
},
|
|
995
|
+
newList: items => this.newList(items),
|
|
996
|
+
newDict: entries => this.newDict(entries),
|
|
997
|
+
random: () => this.random(),
|
|
998
|
+
fail: (code, message, hint) => this.runtimeError(code, message, node, hint),
|
|
999
|
+
emit: event => this.emit({ ...event, nodeId: node.id, span: node.span }),
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
private newList(items: Value[]): ListValue {
|
|
1004
|
+
return { kind: 'lista', id: this.nextId++, items };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** mulberry32: pequeño, rápido y suficiente para ejercicios; da lo mismo en cualquier navegador. */
|
|
1008
|
+
private random(): number {
|
|
1009
|
+
this.randomState = (this.randomState + 0x6d2b79f5) >>> 0;
|
|
1010
|
+
let t = this.randomState;
|
|
1011
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
1012
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
1013
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
private newDict(entries: Map<DictKey, Value>): DictValue {
|
|
1017
|
+
return { kind: 'diccionario', id: this.nextId++, entries };
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/** Revisa que `key` sirva como clave de un diccionario. */
|
|
1021
|
+
private dictKey(key: Value, node: { span: Span }): DictKey {
|
|
1022
|
+
if (typeof key === 'string' || typeof key === 'number') return key;
|
|
1023
|
+
throw this.runtimeError(
|
|
1024
|
+
'CLAVE_INVÁLIDA',
|
|
1025
|
+
`Las claves de un diccionario son textos o números, no ${describeType(typeOf(key))}.`,
|
|
1026
|
+
node,
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// ── operadores ─────────────────────────────────────────────────────────────
|
|
1031
|
+
|
|
1032
|
+
private unary(operator: ast.UnaryOperator, operand: Value, node: ast.Unary): Value {
|
|
1033
|
+
if (operator === '-') {
|
|
1034
|
+
if (typeof operand !== 'number') {
|
|
1035
|
+
throw this.runtimeError(
|
|
1036
|
+
'TIPO_INVÁLIDO',
|
|
1037
|
+
`El signo \`-\` solo funciona con números, no con ${describeType(typeOf(operand))}.`,
|
|
1038
|
+
node,
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
return -operand;
|
|
1042
|
+
}
|
|
1043
|
+
if (typeof operand !== 'boolean') {
|
|
1044
|
+
throw this.runtimeError(
|
|
1045
|
+
'TIPO_INVÁLIDO',
|
|
1046
|
+
`\`no\` solo funciona con \`verdadero\` o \`falso\`, pero recibió ${repr(operand)}.`,
|
|
1047
|
+
node.operand,
|
|
1048
|
+
conditionHint(operand),
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
return !operand;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
private binary(
|
|
1055
|
+
operator: ast.BinaryOperator,
|
|
1056
|
+
left: Value,
|
|
1057
|
+
right: Value,
|
|
1058
|
+
node: ast.Binary | ast.Assignment | ast.Increment,
|
|
1059
|
+
): Value {
|
|
1060
|
+
switch (operator) {
|
|
1061
|
+
case '==':
|
|
1062
|
+
return equals(left, right);
|
|
1063
|
+
case '!=':
|
|
1064
|
+
return !equals(left, right);
|
|
1065
|
+
case '+':
|
|
1066
|
+
if (typeof left === 'number' && typeof right === 'number') return left + right;
|
|
1067
|
+
if (typeof left === 'string' && typeof right === 'string') return left + right;
|
|
1068
|
+
if (typeof left === 'string' || typeof right === 'string') {
|
|
1069
|
+
const other = typeof left === 'string' ? right : left;
|
|
1070
|
+
throw this.runtimeError(
|
|
1071
|
+
'TIPOS_MEZCLADOS',
|
|
1072
|
+
`\`+\` no puede unir un texto con ${describeType(typeOf(other))}.`,
|
|
1073
|
+
node,
|
|
1074
|
+
`Convierte el valor con \`texto(…)\`, o imprime los dos por separado: \`imprimir("Total:", ${repr(other)})\`.`,
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
throw this.operandError(operator, left, right, node);
|
|
1078
|
+
case '-':
|
|
1079
|
+
case '*':
|
|
1080
|
+
case '/':
|
|
1081
|
+
case 'div':
|
|
1082
|
+
case 'mod':
|
|
1083
|
+
case '^': {
|
|
1084
|
+
if (typeof left !== 'number' || typeof right !== 'number') throw this.operandError(operator, left, right, node);
|
|
1085
|
+
if (operator === '-') return left - right;
|
|
1086
|
+
if (operator === '*') return left * right;
|
|
1087
|
+
if (operator === '^') {
|
|
1088
|
+
const result = left ** right;
|
|
1089
|
+
if (Number.isNaN(result)) {
|
|
1090
|
+
throw this.runtimeError(
|
|
1091
|
+
'POTENCIA_INVÁLIDA',
|
|
1092
|
+
`\`${left < 0 ? `(${repr(left)})` : repr(left)} ^ ${repr(right)}\` no tiene un resultado real.`,
|
|
1093
|
+
node,
|
|
1094
|
+
'Un número negativo solo se puede elevar a un exponente entero.',
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
return result;
|
|
1098
|
+
}
|
|
1099
|
+
if (right === 0) {
|
|
1100
|
+
const divisor = node.kind === 'Binary' ? node.right : node.kind === 'Assignment' ? node.value : node;
|
|
1101
|
+
throw this.runtimeError(
|
|
1102
|
+
'DIVISIÓN_ENTRE_CERO',
|
|
1103
|
+
'No se puede dividir entre cero.',
|
|
1104
|
+
divisor,
|
|
1105
|
+
'Revisa que el divisor no valga 0 antes de dividir.',
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
if (operator === '/') return left / right;
|
|
1109
|
+
// `div` y `mod` dividen hacia abajo, así `a == (a div b) * b + (a mod b)` y `-7 mod 3 == 2`.
|
|
1110
|
+
const quotient = Math.floor(left / right);
|
|
1111
|
+
return operator === 'div' ? quotient : left - right * quotient;
|
|
1112
|
+
}
|
|
1113
|
+
case '<':
|
|
1114
|
+
case '>':
|
|
1115
|
+
case '<=':
|
|
1116
|
+
case '>=': {
|
|
1117
|
+
const comparable =
|
|
1118
|
+
(typeof left === 'number' && typeof right === 'number') ||
|
|
1119
|
+
(typeof left === 'string' && typeof right === 'string');
|
|
1120
|
+
if (!comparable) {
|
|
1121
|
+
throw this.runtimeError(
|
|
1122
|
+
'COMPARACIÓN_INVÁLIDA',
|
|
1123
|
+
`No se puede comparar ${describeType(typeOf(left))} con ${describeType(typeOf(right))} usando \`${operator}\`.`,
|
|
1124
|
+
node,
|
|
1125
|
+
'`<`, `>`, `<=` y `>=` comparan dos números o dos textos.',
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
const a = left as number | string;
|
|
1129
|
+
const b = right as number | string;
|
|
1130
|
+
if (operator === '<') return a < b;
|
|
1131
|
+
if (operator === '>') return a > b;
|
|
1132
|
+
if (operator === '<=') return a <= b;
|
|
1133
|
+
return a >= b;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
private index(object: Value, index: Value, node: ast.Index): Value {
|
|
1139
|
+
if (isDomain(object)) return object.scope.variables.get(this.member(object, index, node))!;
|
|
1140
|
+
if (isDict(object)) {
|
|
1141
|
+
const key = this.dictKey(index, node.index);
|
|
1142
|
+
if (!object.entries.has(key)) throw this.missingKey(key, node);
|
|
1143
|
+
return object.entries.get(key)!;
|
|
1144
|
+
}
|
|
1145
|
+
if (node.dot || (!isList(object) && typeof object !== 'string')) {
|
|
1146
|
+
throw this.runtimeError(
|
|
1147
|
+
'ÍNDICE_NO_SOPORTADO',
|
|
1148
|
+
node.dot
|
|
1149
|
+
? `Solo los diccionarios y los dominios tienen campos con \`.\`, no ${describeType(typeOf(object))}.`
|
|
1150
|
+
: `Solo se puede usar \`[ ]\` con listas, textos y diccionarios, no con ${describeType(typeOf(object))}.`,
|
|
1151
|
+
node.object,
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
const position = this.position(object, index, node);
|
|
1155
|
+
return isList(object) ? object.items[position]! : object[position]!;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
private setIndex(
|
|
1159
|
+
object: Value,
|
|
1160
|
+
index: Value,
|
|
1161
|
+
value: Value,
|
|
1162
|
+
target: ast.Index,
|
|
1163
|
+
node: ast.Assignment | ast.Increment,
|
|
1164
|
+
): void {
|
|
1165
|
+
if (typeof object === 'string') {
|
|
1166
|
+
throw this.runtimeError(
|
|
1167
|
+
'TEXTO_INMUTABLE',
|
|
1168
|
+
'Un texto no se puede cambiar letra por letra.',
|
|
1169
|
+
target,
|
|
1170
|
+
'Construye un texto nuevo, por ejemplo uniendo partes con `+`.',
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
if (isDomain(object)) {
|
|
1174
|
+
const name = this.member(object, index, target);
|
|
1175
|
+
if (!object.writable.has(name)) {
|
|
1176
|
+
throw this.runtimeError(
|
|
1177
|
+
'MIEMBRO_DE_SOLO_LECTURA',
|
|
1178
|
+
`\`${object.name}.${name}\` no se puede cambiar desde fuera del dominio.`,
|
|
1179
|
+
target,
|
|
1180
|
+
'Desde fuera de un dominio solo se cambian sus miembros declarados con `variable`; las constantes, los procedimientos y los dominios que contiene quedan fijos.',
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
const before = object.scope.variables.get(name)!;
|
|
1184
|
+
object.scope.variables.set(name, value);
|
|
1185
|
+
if (this.onEvent) {
|
|
1186
|
+
this.emit({
|
|
1187
|
+
type: 'asigna',
|
|
1188
|
+
name: `${object.name}.${name}`,
|
|
1189
|
+
before: serialize(before),
|
|
1190
|
+
after: serialize(value),
|
|
1191
|
+
nodeId: node.id,
|
|
1192
|
+
span: node.span,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (target.dot && !isDict(object)) {
|
|
1198
|
+
throw this.runtimeError(
|
|
1199
|
+
'ÍNDICE_NO_SOPORTADO',
|
|
1200
|
+
`Solo los diccionarios y los dominios tienen campos con \`.\`, no ${describeType(typeOf(object))}.`,
|
|
1201
|
+
target.object,
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
if (isDict(object)) {
|
|
1205
|
+
const key = this.dictKey(index, target.index);
|
|
1206
|
+
const before = object.entries.has(key) ? object.entries.get(key)! : null;
|
|
1207
|
+
object.entries.set(key, value);
|
|
1208
|
+
this.emitModification(object, key, before, value, node);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (!isList(object)) {
|
|
1212
|
+
throw this.runtimeError(
|
|
1213
|
+
'ÍNDICE_NO_SOPORTADO',
|
|
1214
|
+
`Solo se puede cambiar un elemento de una lista o de un diccionario, no de ${describeType(typeOf(object))}.`,
|
|
1215
|
+
target.object,
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
const position = this.position(object, index, target);
|
|
1219
|
+
const before = object.items[position]!;
|
|
1220
|
+
object.items[position] = value;
|
|
1221
|
+
this.emitModification(object, position, before, value, node);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
private emitModification(
|
|
1225
|
+
object: ListValue | DictValue,
|
|
1226
|
+
key: DictKey,
|
|
1227
|
+
before: Value,
|
|
1228
|
+
after: Value,
|
|
1229
|
+
node: ast.Node,
|
|
1230
|
+
): void {
|
|
1231
|
+
if (!this.onEvent) return;
|
|
1232
|
+
this.emit({
|
|
1233
|
+
type: 'modifica',
|
|
1234
|
+
ref: object.id,
|
|
1235
|
+
key: serialize(key),
|
|
1236
|
+
before: serialize(before),
|
|
1237
|
+
after: serialize(after),
|
|
1238
|
+
nodeId: node.id,
|
|
1239
|
+
span: node.span,
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/** El miembro de un dominio al que se refiere `dominio.miembro`; error si no existe o si se usó `[ ]`. */
|
|
1244
|
+
private member(domain: DomainValue, index: Value, node: ast.Index): string {
|
|
1245
|
+
if (!node.dot) {
|
|
1246
|
+
throw this.runtimeError(
|
|
1247
|
+
'ÍNDICE_NO_SOPORTADO',
|
|
1248
|
+
`Los miembros del dominio \`${domain.name}\` se usan con \`.\`, no con \`[ ]\`.`,
|
|
1249
|
+
node,
|
|
1250
|
+
typeof index === 'string' && /^[\p{L}_][\p{L}\d_]*$/u.test(index)
|
|
1251
|
+
? `Escribe \`${domain.name}.${index}\`.`
|
|
1252
|
+
: `Por ejemplo: \`${domain.name}.miembro\`.`,
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
const name = index as string;
|
|
1256
|
+
if (!domain.scope.variables.has(name)) {
|
|
1257
|
+
const suggestion = suggestName(name, [...domain.scope.variables.keys()]);
|
|
1258
|
+
throw this.runtimeError(
|
|
1259
|
+
'MIEMBRO_INEXISTENTE',
|
|
1260
|
+
`El dominio \`${domain.name}\` no tiene nada llamado \`${name}\`.`,
|
|
1261
|
+
node.index,
|
|
1262
|
+
suggestion
|
|
1263
|
+
? `¿Quisiste decir \`${domain.name}.${suggestion}\`?`
|
|
1264
|
+
: 'Los miembros de un dominio se declaran dentro de sus llaves; desde fuera no se pueden agregar.',
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
return name;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
private missingKey(key: DictKey, node: ast.Index): CodexivoError {
|
|
1271
|
+
const written = node.dot ? `\`${key}\`` : repr(key);
|
|
1272
|
+
return this.runtimeError(
|
|
1273
|
+
'CLAVE_INEXISTENTE',
|
|
1274
|
+
`El diccionario no tiene la clave ${written}.`,
|
|
1275
|
+
node.index,
|
|
1276
|
+
`Usa \`tiene(d, ${repr(key)})\` para preguntar antes de leerla.`,
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/** Posición real de `index` (los negativos cuentan desde el final); error si no es válida. */
|
|
1281
|
+
private position(object: ListValue | string, index: Value, node: ast.Index): number {
|
|
1282
|
+
if (typeof index !== 'number' || !Number.isInteger(index)) {
|
|
1283
|
+
throw this.runtimeError(
|
|
1284
|
+
'ÍNDICE_INVÁLIDO',
|
|
1285
|
+
`El índice tiene que ser un número entero, pero es ${repr(index)}.`,
|
|
1286
|
+
node.index,
|
|
1287
|
+
typeof index === 'number'
|
|
1288
|
+
? 'Redondea el índice con `redondear(…)`.'
|
|
1289
|
+
: typeof index === 'string'
|
|
1290
|
+
? 'Las claves de texto son para diccionarios; una lista se recorre con posiciones 0, 1, 2…'
|
|
1291
|
+
: undefined,
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
const length = isList(object) ? object.items.length : object.length;
|
|
1295
|
+
const position = index < 0 ? length + index : index;
|
|
1296
|
+
if (position < 0 || position >= length) {
|
|
1297
|
+
const what = isList(object) ? 'la lista' : 'el texto';
|
|
1298
|
+
const unit = isList(object) ? 'elemento' : 'carácter';
|
|
1299
|
+
throw this.runtimeError(
|
|
1300
|
+
'ÍNDICE_FUERA_DE_RANGO',
|
|
1301
|
+
`El índice ${index} está fuera de ${what}, que tiene ${length} ${length === 1 ? unit : unit === 'carácter' ? 'caracteres' : 'elementos'}.`,
|
|
1302
|
+
node.index,
|
|
1303
|
+
length === 0
|
|
1304
|
+
? isList(object)
|
|
1305
|
+
? 'La lista está vacía.'
|
|
1306
|
+
: 'El texto está vacío.'
|
|
1307
|
+
: `Los índices válidos van de 0 a ${length - 1} (o de -1 a -${length} desde el final).`,
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
return position;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// ── errores ────────────────────────────────────────────────────────────────
|
|
1314
|
+
|
|
1315
|
+
/** Nombres de las llamadas activas. Dentro de la biblioteca solo se ve el procedimiento que llamó el programa. */
|
|
1316
|
+
private callStack(): string[] {
|
|
1317
|
+
return this.frames
|
|
1318
|
+
.filter((frame, index) => !(frame.chunk.hidden && this.frames[index - 1]?.chunk.hidden))
|
|
1319
|
+
.map(frame => frame.name);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* El código de la biblioteca no está en el programa, así que un error ahí se ubica en la llamada del programa
|
|
1324
|
+
* que entró a la biblioteca (`mapear(…)`).
|
|
1325
|
+
*/
|
|
1326
|
+
private visibleSpan(span: Span | undefined): Span | undefined {
|
|
1327
|
+
let index = this.frames.length - 1;
|
|
1328
|
+
if (!this.frames[index]?.chunk.hidden) return span;
|
|
1329
|
+
while (index > 0 && this.frames[index - 1]!.chunk.hidden) index -= 1;
|
|
1330
|
+
return this.frames[index]!.callNode?.span ?? span;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
private runtimeError(code: string, message: string, node: { span: Span }, hint?: string): CodexivoError {
|
|
1334
|
+
return new CodexivoError({
|
|
1335
|
+
kind: 'ejecución',
|
|
1336
|
+
code,
|
|
1337
|
+
message,
|
|
1338
|
+
hint,
|
|
1339
|
+
span: this.visibleSpan(node.span),
|
|
1340
|
+
callStack: this.callStack(),
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
private limitError(code: string, message: string, hint: string, node?: { span: Span }): CodexivoError {
|
|
1345
|
+
return new CodexivoError({
|
|
1346
|
+
kind: 'límite',
|
|
1347
|
+
code,
|
|
1348
|
+
message,
|
|
1349
|
+
hint,
|
|
1350
|
+
span: this.visibleSpan(node?.span ?? this.currentNode?.span),
|
|
1351
|
+
callStack: this.callStack(),
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
private operandError(operator: string, left: Value, right: Value, node: { span: Span }): CodexivoError {
|
|
1356
|
+
return this.runtimeError(
|
|
1357
|
+
'TIPO_INVÁLIDO',
|
|
1358
|
+
`\`${operator}\` no funciona entre ${describeType(typeOf(left))} y ${describeType(typeOf(right))}.`,
|
|
1359
|
+
node,
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
private unknownName(frame: Frame, name: string, node: ast.Identifier): CodexivoError {
|
|
1364
|
+
const visible: string[] = [
|
|
1365
|
+
...Object.keys(this.natives),
|
|
1366
|
+
...Object.keys(libraryProcedures),
|
|
1367
|
+
...Object.keys(libraryConstants),
|
|
1368
|
+
];
|
|
1369
|
+
for (let scope: Scope | undefined = frame.scope; scope; scope = scope.parent)
|
|
1370
|
+
visible.push(...scope.variables.keys());
|
|
1371
|
+
const suggestion = suggestName(name, visible);
|
|
1372
|
+
return this.runtimeError(
|
|
1373
|
+
'NOMBRE_DESCONOCIDO',
|
|
1374
|
+
`No existe nada llamado \`${name}\`.`,
|
|
1375
|
+
node,
|
|
1376
|
+
suggestion
|
|
1377
|
+
? `¿Quisiste decir \`${suggestion}\`?`
|
|
1378
|
+
: `Si es una variable nueva, declárala con \`variable ${name} = …\`.`,
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
private constantReassignment(name: string, node: ast.Assignment | ast.Increment): CodexivoError {
|
|
1383
|
+
return this.runtimeError(
|
|
1384
|
+
'CONSTANTE_REASIGNADA',
|
|
1385
|
+
`\`${name}\` es una constante; su valor no puede cambiar.`,
|
|
1386
|
+
node,
|
|
1387
|
+
`Si necesitas cambiarlo, declárala con \`variable ${name} = …\` en lugar de \`constante\`.`,
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
private invalidRepeatCount(count: Value, node: ast.Expression): CodexivoError {
|
|
1392
|
+
if (typeof count !== 'number') {
|
|
1393
|
+
return this.runtimeError(
|
|
1394
|
+
'REPETICIONES_INVÁLIDAS',
|
|
1395
|
+
`\`repetir\` necesita un número de veces, pero recibió ${describeType(typeOf(count))} (${repr(count)}).`,
|
|
1396
|
+
node,
|
|
1397
|
+
typeof count === 'string' ? 'Convierte el texto con `número(…)`.' : undefined,
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
return this.runtimeError(
|
|
1401
|
+
'REPETICIONES_INVÁLIDAS',
|
|
1402
|
+
`\`repetir\` necesita un número entero de veces, 0 o más, pero recibió ${repr(count)}.`,
|
|
1403
|
+
node,
|
|
1404
|
+
count < 0 ? 'Revisa que la cantidad no sea negativa.' : 'Redondea la cantidad con `redondear(…)`.',
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
private undeclaredAssignment(name: string, node: ast.Identifier): CodexivoError {
|
|
1409
|
+
if (Object.hasOwn(libraryConstants, name)) {
|
|
1410
|
+
return this.runtimeError(
|
|
1411
|
+
'NOMBRE_RESERVADO_POR_BIBLIOTECA',
|
|
1412
|
+
`\`${name}\` es una constante de Codexivo; no se le puede asignar un valor.`,
|
|
1413
|
+
node,
|
|
1414
|
+
`Si quieres tu propia variable con ese nombre, declárala: \`variable ${name} = …\`.`,
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
if (this.natives[name] || libraryProcedures[name]) {
|
|
1418
|
+
return this.runtimeError(
|
|
1419
|
+
'NOMBRE_RESERVADO_POR_BIBLIOTECA',
|
|
1420
|
+
`\`${name}\` es un procedimiento de Codexivo; no se le puede asignar un valor.`,
|
|
1421
|
+
node,
|
|
1422
|
+
`Si quieres tu propia variable con ese nombre, declárala: \`variable ${name} = …\`.`,
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
return this.runtimeError(
|
|
1426
|
+
'VARIABLE_NO_DECLARADA',
|
|
1427
|
+
`La variable \`${name}\` no existe.`,
|
|
1428
|
+
node,
|
|
1429
|
+
`¿Olvidaste declararla? Escribe \`variable ${name} = …\` la primera vez.`,
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
private nonBooleanCondition(label: DecisionLabel, value: Value, node: ast.Expression): CodexivoError {
|
|
1434
|
+
return this.runtimeError(
|
|
1435
|
+
'CONDICIÓN_NO_LÓGICA',
|
|
1436
|
+
`La condición de \`${label}\` tiene que ser \`verdadero\` o \`falso\`, pero vale ${repr(value)}.`,
|
|
1437
|
+
node,
|
|
1438
|
+
conditionHint(value),
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
private nonBooleanLogical(node: ast.Logical, value: Value, side: ast.Expression): CodexivoError {
|
|
1443
|
+
return this.runtimeError(
|
|
1444
|
+
'OPERANDO_NO_LÓGICO',
|
|
1445
|
+
`\`${node.operator}\` une valores \`verdadero\` o \`falso\`, pero recibió ${repr(value)}.`,
|
|
1446
|
+
side,
|
|
1447
|
+
conditionHint(value),
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
private emitOperation(operator: string, operands: Value[], result: Value, node: ast.Node): void {
|
|
1452
|
+
this.emit({
|
|
1453
|
+
type: 'opera',
|
|
1454
|
+
operator,
|
|
1455
|
+
operands: operands.map(serialize),
|
|
1456
|
+
result: serialize(result),
|
|
1457
|
+
nodeId: node.id,
|
|
1458
|
+
span: node.span,
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
private emit(event: DistributiveOmit<MachineEvent, 'operations' | 'depth'>): void {
|
|
1463
|
+
if (!this.onEvent) return;
|
|
1464
|
+
// Dentro de la biblioteca solo se ven las llamadas (a `f` y de regreso), la salida, los errores y el fin.
|
|
1465
|
+
if (this.frames[this.frames.length - 1]?.chunk.hidden && !VISIBLE_FROM_LIBRARY.has(event.type)) return;
|
|
1466
|
+
this.onEvent({ ...event, operations: this.operations, depth: this.frames.length } as MachineEvent);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
1471
|
+
|
|
1472
|
+
const splitLines = (text: string): string[] => text.replace(/\r\n/g, '\n').replace(/\n$/, '').split('\n');
|
|
1473
|
+
|
|
1474
|
+
const formatCount = (count: number): string => count.toLocaleString('es-MX').replace(/,/g, ' ');
|
|
1475
|
+
|
|
1476
|
+
const describeCallee = (node: ast.Call): string => {
|
|
1477
|
+
const { callee } = node;
|
|
1478
|
+
if (callee.kind === 'Identifier') return callee.name;
|
|
1479
|
+
if (
|
|
1480
|
+
callee.kind === 'Index' &&
|
|
1481
|
+
callee.dot &&
|
|
1482
|
+
callee.object.kind === 'Identifier' &&
|
|
1483
|
+
callee.index.kind === 'StringLiteral'
|
|
1484
|
+
) {
|
|
1485
|
+
return `${callee.object.name}.${callee.index.value}`;
|
|
1486
|
+
}
|
|
1487
|
+
return 'esto';
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
const conditionHint = (value: Value): string | undefined => {
|
|
1491
|
+
switch (typeOf(value)) {
|
|
1492
|
+
case 'número':
|
|
1493
|
+
return `¿Quisiste comparar? Por ejemplo: \`x es_distinto_de 0\` o \`x es_mayor_que 0\`.`;
|
|
1494
|
+
case 'texto':
|
|
1495
|
+
return '¿Quisiste comparar? Por ejemplo: `texto es_distinto_de ""`.';
|
|
1496
|
+
case 'lista':
|
|
1497
|
+
return '¿Quisiste decir `longitud(lista) es_mayor_que 0`?';
|
|
1498
|
+
case 'diccionario':
|
|
1499
|
+
return '¿Quisiste decir `longitud(d) es_mayor_que 0` o `tiene(d, clave)`?';
|
|
1500
|
+
case 'nulo':
|
|
1501
|
+
return '¿Quisiste decir `x es_distinto_de nulo`?';
|
|
1502
|
+
default:
|
|
1503
|
+
return undefined;
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1507
|
+
const checkArity = (
|
|
1508
|
+
expected: number | [number, number],
|
|
1509
|
+
received: number,
|
|
1510
|
+
name: string,
|
|
1511
|
+
fail: (message: string, hint?: string) => CodexivoError,
|
|
1512
|
+
): void => {
|
|
1513
|
+
const [min, max] = typeof expected === 'number' ? [expected, expected] : expected;
|
|
1514
|
+
if (received >= min && received <= max) return;
|
|
1515
|
+
const plural = (count: number) => (count === 1 ? '1 valor' : `${count} valores`);
|
|
1516
|
+
let wanted: string;
|
|
1517
|
+
if (min === max) wanted = plural(min);
|
|
1518
|
+
else if (max === Infinity) wanted = `al menos ${plural(min)}`;
|
|
1519
|
+
else wanted = `entre ${min} y ${plural(max)}`;
|
|
1520
|
+
throw fail(`\`${name}\` espera ${wanted}, pero recibió ${received}.`);
|
|
1521
|
+
};
|