codexivo 0.2.0 → 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 +1 -1
- package/README.md +26 -25
- package/dist/index.js +151 -9
- package/dist/index.js.map +22 -12
- package/dist/worker.js +153 -0
- package/dist/worker.js.map +25 -0
- package/docs/README.md +2 -0
- package/docs/arquitectura.md +86 -4
- package/docs/especificacion-v1.md +192 -86
- package/docs/guia-aprender-desde-el-codigo.md +5 -5
- package/docs/integracion.md +104 -0
- package/docs/lenguaje.md +435 -60
- package/docs/modo-mundo.md +128 -0
- package/docs/recetario.md +63 -63
- package/package.json +11 -20
- package/src/catalog.ts +256 -0
- package/src/challenge.ts +138 -0
- package/src/engine.ts +7 -0
- package/src/error-codes.ts +427 -0
- package/src/flow.ts +385 -0
- package/src/index.ts +53 -1
- package/src/printer.ts +133 -0
- package/src/runtime/builtins.ts +1358 -65
- package/src/runtime/compiler.ts +278 -31
- package/src/runtime/costs.ts +96 -0
- package/src/runtime/machine.ts +722 -48
- package/src/runtime/prelude.ts +230 -0
- package/src/runtime/values.ts +119 -18
- package/src/sandbox-host.ts +310 -0
- package/src/sandbox.ts +95 -0
- package/src/syntax/ast.ts +120 -6
- package/src/syntax/lexer.ts +39 -5
- package/src/syntax/parser.ts +496 -72
- package/src/syntax/token.ts +49 -1
- package/src/worker.ts +18 -0
- package/src/world.ts +518 -0
- package/bin/codexivo.exe +0 -1
- package/postinstall.mjs +0 -38
package/src/runtime/machine.ts
CHANGED
|
@@ -2,10 +2,33 @@ import { CodexivoError } from '../errors';
|
|
|
2
2
|
import type { ErrorInfo } from '../errors';
|
|
3
3
|
import type * as ast from '../syntax/ast';
|
|
4
4
|
import type { Span } from '../syntax/token';
|
|
5
|
-
import {
|
|
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';
|
|
6
9
|
import type { Chunk, DecisionLabel, Instruction } from './compiler';
|
|
7
|
-
import {
|
|
8
|
-
|
|
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';
|
|
9
32
|
import { suggestName } from './suggest';
|
|
10
33
|
|
|
11
34
|
export type MachineStatus = 'lista' | 'pausada' | 'esperando_entrada' | 'terminada' | 'error';
|
|
@@ -21,14 +44,21 @@ export interface MachineOptions {
|
|
|
21
44
|
input?: string | string[];
|
|
22
45
|
/** Si es `true`, `leer()` sin entrada pausa la máquina en lugar de regresar `nulo`. */
|
|
23
46
|
interactiveInput?: boolean;
|
|
24
|
-
/** Procedimientos nativos adicionales
|
|
47
|
+
/** Procedimientos nativos adicionales. */
|
|
25
48
|
natives?: Record<string, NativeValue>;
|
|
49
|
+
/** Mundo del robot (de `createWorld`): agrega sus procedimientos y su estado a `snapshot()`. */
|
|
50
|
+
world?: World;
|
|
26
51
|
/** Líneas donde `run()` se detiene antes de ejecutar la sentencia. */
|
|
27
52
|
breakpoints?: number[];
|
|
28
53
|
/** Alcance global compartido entre ejecuciones (lo usa el REPL). */
|
|
29
54
|
globals?: Scope;
|
|
30
55
|
/** Permite volver a declarar una variable existente (útil en el REPL). */
|
|
31
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;
|
|
32
62
|
/** Recibe cada evento de la ejecución. Activarlo hace la ejecución más lenta. */
|
|
33
63
|
onEvent?: (event: MachineEvent) => void;
|
|
34
64
|
}
|
|
@@ -41,8 +71,17 @@ export type SerializedValue =
|
|
|
41
71
|
| { type: 'lógico'; value: boolean; repr: string }
|
|
42
72
|
| { type: 'nulo'; value: null; repr: string }
|
|
43
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 }
|
|
44
76
|
| { type: 'procedimiento'; name?: string; repr: string };
|
|
45
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
|
+
|
|
46
85
|
export const serialize = (value: Value): SerializedValue => {
|
|
47
86
|
const text = repr(value);
|
|
48
87
|
if (value === null) return { type: 'nulo', value: null, repr: text };
|
|
@@ -55,6 +94,8 @@ export const serialize = (value: Value): SerializedValue => {
|
|
|
55
94
|
return { type: 'lógico', value, repr: text };
|
|
56
95
|
}
|
|
57
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 };
|
|
58
99
|
return { type: 'procedimiento', name: value.name, repr: text };
|
|
59
100
|
};
|
|
60
101
|
|
|
@@ -68,8 +109,18 @@ interface EventBase {
|
|
|
68
109
|
export type MachineEvent = EventBase &
|
|
69
110
|
(
|
|
70
111
|
| { type: 'paso'; nodeId: number; nodeKind: ast.Statement['kind']; span: Span }
|
|
71
|
-
| { type: 'declara'; name: string; value: SerializedValue; nodeId: number; span: Span }
|
|
112
|
+
| { type: 'declara'; name: string; constant: boolean; value: SerializedValue; nodeId: number; span: Span }
|
|
72
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
|
+
}
|
|
73
124
|
| { type: 'decide'; label: DecisionLabel; value: boolean; nodeId: number; ownerId: number; span: Span }
|
|
74
125
|
| {
|
|
75
126
|
type: 'opera';
|
|
@@ -81,6 +132,7 @@ export type MachineEvent = EventBase &
|
|
|
81
132
|
}
|
|
82
133
|
| { type: 'llama'; name: string; args: SerializedValue[]; nodeId: number; span: Span }
|
|
83
134
|
| { type: 'regresa'; name: string; value: SerializedValue }
|
|
135
|
+
| (RobotEvent & { nodeId: number; span: Span })
|
|
84
136
|
| { type: 'imprime'; text: string }
|
|
85
137
|
| { type: 'lee'; text: string | null }
|
|
86
138
|
| { type: 'error'; error: ErrorInfo }
|
|
@@ -90,6 +142,8 @@ export type MachineEvent = EventBase &
|
|
|
90
142
|
export interface FrameSnapshot {
|
|
91
143
|
name: string;
|
|
92
144
|
callSpan?: Span;
|
|
145
|
+
/** `true` en un procedimiento de la biblioteca escrito en Codexivo; sus variables no se muestran. */
|
|
146
|
+
library?: boolean;
|
|
93
147
|
/** Alcances del procedimiento, del más externo al más interno. */
|
|
94
148
|
scopes: Array<Record<string, SerializedValue>>;
|
|
95
149
|
}
|
|
@@ -97,14 +151,18 @@ export interface FrameSnapshot {
|
|
|
97
151
|
export interface Snapshot {
|
|
98
152
|
status: MachineStatus;
|
|
99
153
|
operations: number;
|
|
154
|
+
/** Puntos de pausa por los que ya pasó la ejecución (ver `Machine.steps`). */
|
|
155
|
+
steps: number;
|
|
100
156
|
/** Nodo donde está detenida la máquina. */
|
|
101
157
|
nodeId?: number;
|
|
102
158
|
span?: Span;
|
|
103
159
|
/** Llamadas activas, del programa principal a la más reciente. */
|
|
104
160
|
frames: FrameSnapshot[];
|
|
105
161
|
/** Contenido de las listas que aparecen en `frames`, por `ref`. */
|
|
106
|
-
heap: Record<number,
|
|
162
|
+
heap: Record<number, HeapEntry>;
|
|
107
163
|
output: string;
|
|
164
|
+
/** Estado del mundo, si la máquina corre en modo mundo. */
|
|
165
|
+
world?: WorldState;
|
|
108
166
|
}
|
|
109
167
|
|
|
110
168
|
// ── máquina ──────────────────────────────────────────────────────────────────
|
|
@@ -127,7 +185,20 @@ const DEFAULTS = {
|
|
|
127
185
|
maxOutput: 1_000_000,
|
|
128
186
|
};
|
|
129
187
|
|
|
130
|
-
const
|
|
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
|
+
]);
|
|
131
202
|
|
|
132
203
|
/**
|
|
133
204
|
* Ejecuta un programa compilado. Tiene su propia pila de llamadas, así que la recursión no depende
|
|
@@ -138,7 +209,7 @@ export class Machine {
|
|
|
138
209
|
private readonly stack: Value[] = [];
|
|
139
210
|
private readonly natives: Record<string, NativeValue>;
|
|
140
211
|
private readonly inputQueue: string[];
|
|
141
|
-
private
|
|
212
|
+
private breakpoints: Set<number>;
|
|
142
213
|
private readonly limits: typeof DEFAULTS;
|
|
143
214
|
private readonly onEvent?: (event: MachineEvent) => void;
|
|
144
215
|
private readonly outputParts: string[] = [];
|
|
@@ -148,8 +219,19 @@ export class Machine {
|
|
|
148
219
|
private inputClosed = false;
|
|
149
220
|
private currentNode?: ast.Node;
|
|
150
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
|
+
|
|
151
226
|
status: MachineStatus = 'lista';
|
|
152
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';
|
|
153
235
|
result: Value = null;
|
|
154
236
|
error?: CodexivoError;
|
|
155
237
|
|
|
@@ -157,7 +239,7 @@ export class Machine {
|
|
|
157
239
|
program: Chunk,
|
|
158
240
|
private readonly options: MachineOptions = {},
|
|
159
241
|
) {
|
|
160
|
-
this.natives = { ...defaultNatives, ...options.natives };
|
|
242
|
+
this.natives = { ...defaultNatives, ...options.world?.natives, ...options.natives };
|
|
161
243
|
this.limits = {
|
|
162
244
|
maxOperations: options.maxOperations ?? DEFAULTS.maxOperations,
|
|
163
245
|
maxCallDepth: options.maxCallDepth ?? DEFAULTS.maxCallDepth,
|
|
@@ -166,6 +248,9 @@ export class Machine {
|
|
|
166
248
|
this.inputQueue = typeof options.input === 'string' ? splitLines(options.input) : [...(options.input ?? [])];
|
|
167
249
|
this.breakpoints = new Set(options.breakpoints ?? []);
|
|
168
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;
|
|
169
254
|
const globals = options.globals ?? new Scope();
|
|
170
255
|
this.frames.push({ chunk: program, pc: 0, scope: globals, root: globals, stackBase: 0, name: 'programa' });
|
|
171
256
|
}
|
|
@@ -189,6 +274,11 @@ export class Machine {
|
|
|
189
274
|
return this.execute('step', Infinity);
|
|
190
275
|
}
|
|
191
276
|
|
|
277
|
+
/** Cambia las líneas donde `run()` se detiene. */
|
|
278
|
+
setBreakpoints(lines: number[]): void {
|
|
279
|
+
this.breakpoints = new Set(lines);
|
|
280
|
+
}
|
|
281
|
+
|
|
192
282
|
/** Entrega una línea a un `leer()` que está esperando. */
|
|
193
283
|
provideInput(line: string): void {
|
|
194
284
|
this.inputQueue.push(...splitLines(line));
|
|
@@ -202,11 +292,28 @@ export class Machine {
|
|
|
202
292
|
}
|
|
203
293
|
|
|
204
294
|
snapshot(): Snapshot {
|
|
205
|
-
const heap: Record<number,
|
|
295
|
+
const heap: Record<number, HeapEntry> = {};
|
|
206
296
|
const collect = (value: Value) => {
|
|
207
|
-
if (!isList(value) || heap[value.id]) return;
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
}
|
|
210
317
|
};
|
|
211
318
|
const frames = this.frames.map(frame => {
|
|
212
319
|
const scopes: Array<Record<string, SerializedValue>> = [];
|
|
@@ -221,16 +328,19 @@ export class Machine {
|
|
|
221
328
|
if (scope === frame.root) break;
|
|
222
329
|
scope = scope.parent;
|
|
223
330
|
}
|
|
331
|
+
if (frame.chunk.hidden) return { name: frame.name, callSpan: frame.callNode?.span, library: true, scopes: [] };
|
|
224
332
|
return { name: frame.name, callSpan: frame.callNode?.span, scopes };
|
|
225
333
|
});
|
|
226
334
|
return {
|
|
227
335
|
status: this.status,
|
|
228
336
|
operations: this.operations,
|
|
337
|
+
steps: this.steps,
|
|
229
338
|
nodeId: this.currentNode?.id,
|
|
230
339
|
span: this.currentNode?.span,
|
|
231
340
|
frames,
|
|
232
341
|
heap,
|
|
233
342
|
output: this.output,
|
|
343
|
+
...(this.options.world ? { world: this.options.world.state() } : {}),
|
|
234
344
|
};
|
|
235
345
|
}
|
|
236
346
|
|
|
@@ -246,7 +356,7 @@ export class Machine {
|
|
|
246
356
|
const frame = this.frames[this.frames.length - 1]!;
|
|
247
357
|
const instruction = frame.chunk.code[frame.pc]!;
|
|
248
358
|
|
|
249
|
-
if (!NO_COUNT.has(instruction.op)) {
|
|
359
|
+
if (!NO_COUNT.has(instruction.op) && !frame.chunk.hidden) {
|
|
250
360
|
if (this.operations >= this.limits.maxOperations) {
|
|
251
361
|
throw this.limitError(
|
|
252
362
|
'DEMASIADAS_OPERACIONES',
|
|
@@ -256,6 +366,7 @@ export class Machine {
|
|
|
256
366
|
}
|
|
257
367
|
if (this.operations >= stopAt) {
|
|
258
368
|
this.status = 'pausada';
|
|
369
|
+
this.pauseReason = 'presupuesto';
|
|
259
370
|
return this.status;
|
|
260
371
|
}
|
|
261
372
|
}
|
|
@@ -264,10 +375,12 @@ export class Machine {
|
|
|
264
375
|
if (
|
|
265
376
|
instruction.op === 'step' &&
|
|
266
377
|
mode === 'run' &&
|
|
378
|
+
!frame.chunk.hidden &&
|
|
267
379
|
executed > 0 &&
|
|
268
380
|
this.breakpoints.has(instruction.node.span.start.line)
|
|
269
381
|
) {
|
|
270
382
|
this.status = 'pausada';
|
|
383
|
+
this.pauseReason = 'punto_de_interrupción';
|
|
271
384
|
return this.status;
|
|
272
385
|
}
|
|
273
386
|
|
|
@@ -276,9 +389,14 @@ export class Machine {
|
|
|
276
389
|
// `exec` puede cambiar el estado.
|
|
277
390
|
const status = this.status as MachineStatus;
|
|
278
391
|
if (status === 'esperando_entrada' || status === 'terminada') return status;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
}
|
|
282
400
|
}
|
|
283
401
|
}
|
|
284
402
|
} catch (error) {
|
|
@@ -293,10 +411,11 @@ export class Machine {
|
|
|
293
411
|
/** Ejecuta una instrucción. Regresa `true` si es un punto de pausa para `step()`. */
|
|
294
412
|
private exec(frame: Frame, instruction: Instruction): boolean {
|
|
295
413
|
frame.pc += 1;
|
|
296
|
-
if (!NO_COUNT.has(instruction.op)) this.operations += 1;
|
|
414
|
+
if (!NO_COUNT.has(instruction.op) && !frame.chunk.hidden) this.operations += 1;
|
|
297
415
|
|
|
298
416
|
switch (instruction.op) {
|
|
299
417
|
case 'step':
|
|
418
|
+
if (frame.chunk.hidden) return false;
|
|
300
419
|
this.currentNode = instruction.node;
|
|
301
420
|
this.emit({
|
|
302
421
|
type: 'paso',
|
|
@@ -316,14 +435,28 @@ export class Machine {
|
|
|
316
435
|
return false;
|
|
317
436
|
}
|
|
318
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
|
+
|
|
319
448
|
case 'load': {
|
|
320
449
|
const scope = frame.scope.lookup(instruction.name);
|
|
321
450
|
if (scope) {
|
|
322
451
|
this.stack.push(scope.variables.get(instruction.name)!);
|
|
323
452
|
return false;
|
|
324
453
|
}
|
|
325
|
-
const native =
|
|
326
|
-
|
|
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) {
|
|
327
460
|
this.stack.push(native);
|
|
328
461
|
return false;
|
|
329
462
|
}
|
|
@@ -337,14 +470,19 @@ export class Machine {
|
|
|
337
470
|
throw this.runtimeError(
|
|
338
471
|
'VARIABLE_REPETIDA',
|
|
339
472
|
`La variable \`${instruction.name}\` ya existe en este bloque.`,
|
|
340
|
-
instruction.
|
|
473
|
+
instruction.target,
|
|
341
474
|
`Para cambiar su valor escribe \`${instruction.name} = …\`, sin \`variable\`.`,
|
|
342
475
|
);
|
|
343
476
|
}
|
|
344
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);
|
|
345
482
|
this.emit({
|
|
346
483
|
type: 'declara',
|
|
347
484
|
name: instruction.name,
|
|
485
|
+
constant: instruction.constant,
|
|
348
486
|
value: serialize(value),
|
|
349
487
|
nodeId: instruction.node.id,
|
|
350
488
|
span: instruction.node.span,
|
|
@@ -355,7 +493,26 @@ export class Machine {
|
|
|
355
493
|
case 'store': {
|
|
356
494
|
const value = this.stack.pop()!;
|
|
357
495
|
const scope = frame.scope.lookup(instruction.name);
|
|
358
|
-
if (!scope) throw this.undeclaredAssignment(instruction.name, instruction.
|
|
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
|
+
}
|
|
359
516
|
const before = scope.variables.get(instruction.name)!;
|
|
360
517
|
scope.variables.set(instruction.name, value);
|
|
361
518
|
if (this.onEvent) {
|
|
@@ -371,6 +528,22 @@ export class Machine {
|
|
|
371
528
|
return false;
|
|
372
529
|
}
|
|
373
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
|
+
|
|
374
547
|
case 'result': {
|
|
375
548
|
const value = this.stack.pop()!;
|
|
376
549
|
if (this.frames.length === 1) this.lastValue = value;
|
|
@@ -422,7 +595,10 @@ export class Machine {
|
|
|
422
595
|
this.finish(value);
|
|
423
596
|
return true;
|
|
424
597
|
}
|
|
425
|
-
|
|
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
|
+
}
|
|
426
602
|
this.frames.pop();
|
|
427
603
|
this.stack.length = frame.stackBase - 1;
|
|
428
604
|
this.stack.push(value);
|
|
@@ -437,7 +613,7 @@ export class Machine {
|
|
|
437
613
|
const condition = this.stack.pop()!;
|
|
438
614
|
if (typeof condition !== 'boolean')
|
|
439
615
|
throw this.nonBooleanCondition(instruction.label, condition, instruction.node);
|
|
440
|
-
this.currentNode = instruction.node;
|
|
616
|
+
if (!frame.chunk.hidden) this.currentNode = instruction.node;
|
|
441
617
|
this.emit({
|
|
442
618
|
type: 'decide',
|
|
443
619
|
label: instruction.label,
|
|
@@ -450,6 +626,186 @@ export class Machine {
|
|
|
450
626
|
return true;
|
|
451
627
|
}
|
|
452
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
|
+
|
|
453
809
|
case 'shortCircuit': {
|
|
454
810
|
const left = this.stack[this.stack.length - 1]!;
|
|
455
811
|
if (typeof left !== 'boolean') throw this.nonBooleanLogical(instruction.node, left, instruction.node.left);
|
|
@@ -474,9 +830,41 @@ export class Machine {
|
|
|
474
830
|
}
|
|
475
831
|
|
|
476
832
|
case 'enterScope':
|
|
833
|
+
case 'domainStart':
|
|
477
834
|
frame.scope = new Scope(frame.scope);
|
|
478
835
|
return false;
|
|
479
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
|
+
|
|
480
868
|
case 'exitScope':
|
|
481
869
|
frame.scope = frame.scope.parent ?? frame.scope;
|
|
482
870
|
return false;
|
|
@@ -512,9 +900,22 @@ export class Machine {
|
|
|
512
900
|
}
|
|
513
901
|
|
|
514
902
|
const args = this.stack.slice(calleeIndex + 1);
|
|
515
|
-
const expected =
|
|
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;
|
|
516
909
|
checkArity(expected, argc, name, (message, hint) =>
|
|
517
|
-
this.runtimeError(
|
|
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
|
+
),
|
|
518
919
|
);
|
|
519
920
|
|
|
520
921
|
if (callee.kind === 'nativo') {
|
|
@@ -522,10 +923,11 @@ export class Machine {
|
|
|
522
923
|
if (result === WAIT_FOR_INPUT) {
|
|
523
924
|
// Repetimos la llamada cuando llegue la entrada.
|
|
524
925
|
frame.pc -= 1;
|
|
525
|
-
this.operations -= 1;
|
|
926
|
+
if (!frame.chunk.hidden) this.operations -= 1;
|
|
526
927
|
this.status = 'esperando_entrada';
|
|
527
928
|
return true;
|
|
528
929
|
}
|
|
930
|
+
if (!frame.chunk.hidden) this.charge(callee.cost, args);
|
|
529
931
|
this.stack.length = calleeIndex;
|
|
530
932
|
this.stack.push(result);
|
|
531
933
|
return false;
|
|
@@ -541,9 +943,13 @@ export class Machine {
|
|
|
541
943
|
);
|
|
542
944
|
}
|
|
543
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);
|
|
544
948
|
const scope = new Scope(callee.closure);
|
|
545
|
-
callee.chunk.params.forEach((param, index) => scope.variables.set(param, args[index]
|
|
546
|
-
|
|
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
|
+
}
|
|
547
953
|
this.frames.push({
|
|
548
954
|
chunk: callee.chunk,
|
|
549
955
|
pc: 0,
|
|
@@ -556,6 +962,11 @@ export class Machine {
|
|
|
556
962
|
return false;
|
|
557
963
|
}
|
|
558
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
|
+
|
|
559
970
|
private nativeContext(node: ast.Call): NativeContext {
|
|
560
971
|
return {
|
|
561
972
|
write: text => {
|
|
@@ -582,7 +993,10 @@ export class Machine {
|
|
|
582
993
|
return null;
|
|
583
994
|
},
|
|
584
995
|
newList: items => this.newList(items),
|
|
996
|
+
newDict: entries => this.newDict(entries),
|
|
997
|
+
random: () => this.random(),
|
|
585
998
|
fail: (code, message, hint) => this.runtimeError(code, message, node, hint),
|
|
999
|
+
emit: event => this.emit({ ...event, nodeId: node.id, span: node.span }),
|
|
586
1000
|
};
|
|
587
1001
|
}
|
|
588
1002
|
|
|
@@ -590,6 +1004,29 @@ export class Machine {
|
|
|
590
1004
|
return { kind: 'lista', id: this.nextId++, items };
|
|
591
1005
|
}
|
|
592
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
|
+
|
|
593
1030
|
// ── operadores ─────────────────────────────────────────────────────────────
|
|
594
1031
|
|
|
595
1032
|
private unary(operator: ast.UnaryOperator, operand: Value, node: ast.Unary): Value {
|
|
@@ -614,7 +1051,12 @@ export class Machine {
|
|
|
614
1051
|
return !operand;
|
|
615
1052
|
}
|
|
616
1053
|
|
|
617
|
-
private binary(
|
|
1054
|
+
private binary(
|
|
1055
|
+
operator: ast.BinaryOperator,
|
|
1056
|
+
left: Value,
|
|
1057
|
+
right: Value,
|
|
1058
|
+
node: ast.Binary | ast.Assignment | ast.Increment,
|
|
1059
|
+
): Value {
|
|
618
1060
|
switch (operator) {
|
|
619
1061
|
case '==':
|
|
620
1062
|
return equals(left, right);
|
|
@@ -635,12 +1077,27 @@ export class Machine {
|
|
|
635
1077
|
throw this.operandError(operator, left, right, node);
|
|
636
1078
|
case '-':
|
|
637
1079
|
case '*':
|
|
638
|
-
case '/':
|
|
1080
|
+
case '/':
|
|
1081
|
+
case 'div':
|
|
1082
|
+
case 'mod':
|
|
1083
|
+
case '^': {
|
|
639
1084
|
if (typeof left !== 'number' || typeof right !== 'number') throw this.operandError(operator, left, right, node);
|
|
640
1085
|
if (operator === '-') return left - right;
|
|
641
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
|
+
}
|
|
642
1099
|
if (right === 0) {
|
|
643
|
-
const divisor = node.kind === 'Binary' ? node.right : node.value;
|
|
1100
|
+
const divisor = node.kind === 'Binary' ? node.right : node.kind === 'Assignment' ? node.value : node;
|
|
644
1101
|
throw this.runtimeError(
|
|
645
1102
|
'DIVISIÓN_ENTRE_CERO',
|
|
646
1103
|
'No se puede dividir entre cero.',
|
|
@@ -648,7 +1105,10 @@ export class Machine {
|
|
|
648
1105
|
'Revisa que el divisor no valga 0 antes de dividir.',
|
|
649
1106
|
);
|
|
650
1107
|
}
|
|
651
|
-
return left / right;
|
|
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;
|
|
652
1112
|
}
|
|
653
1113
|
case '<':
|
|
654
1114
|
case '>':
|
|
@@ -676,19 +1136,159 @@ export class Machine {
|
|
|
676
1136
|
}
|
|
677
1137
|
|
|
678
1138
|
private index(object: Value, index: Value, node: ast.Index): Value {
|
|
679
|
-
if (
|
|
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')) {
|
|
680
1146
|
throw this.runtimeError(
|
|
681
1147
|
'ÍNDICE_NO_SOPORTADO',
|
|
682
|
-
|
|
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))}.`,
|
|
683
1151
|
node.object,
|
|
684
1152
|
);
|
|
685
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 {
|
|
686
1282
|
if (typeof index !== 'number' || !Number.isInteger(index)) {
|
|
687
1283
|
throw this.runtimeError(
|
|
688
1284
|
'ÍNDICE_INVÁLIDO',
|
|
689
1285
|
`El índice tiene que ser un número entero, pero es ${repr(index)}.`,
|
|
690
1286
|
node.index,
|
|
691
|
-
typeof index === 'number'
|
|
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,
|
|
692
1292
|
);
|
|
693
1293
|
}
|
|
694
1294
|
const length = isList(object) ? object.items.length : object.length;
|
|
@@ -701,21 +1301,44 @@ export class Machine {
|
|
|
701
1301
|
`El índice ${index} está fuera de ${what}, que tiene ${length} ${length === 1 ? unit : unit === 'carácter' ? 'caracteres' : 'elementos'}.`,
|
|
702
1302
|
node.index,
|
|
703
1303
|
length === 0
|
|
704
|
-
?
|
|
1304
|
+
? isList(object)
|
|
1305
|
+
? 'La lista está vacía.'
|
|
1306
|
+
: 'El texto está vacío.'
|
|
705
1307
|
: `Los índices válidos van de 0 a ${length - 1} (o de -1 a -${length} desde el final).`,
|
|
706
1308
|
);
|
|
707
1309
|
}
|
|
708
|
-
return
|
|
1310
|
+
return position;
|
|
709
1311
|
}
|
|
710
1312
|
|
|
711
1313
|
// ── errores ────────────────────────────────────────────────────────────────
|
|
712
1314
|
|
|
1315
|
+
/** Nombres de las llamadas activas. Dentro de la biblioteca solo se ve el procedimiento que llamó el programa. */
|
|
713
1316
|
private callStack(): string[] {
|
|
714
|
-
return this.frames
|
|
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;
|
|
715
1331
|
}
|
|
716
1332
|
|
|
717
1333
|
private runtimeError(code: string, message: string, node: { span: Span }, hint?: string): CodexivoError {
|
|
718
|
-
return new 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
|
+
});
|
|
719
1342
|
}
|
|
720
1343
|
|
|
721
1344
|
private limitError(code: string, message: string, hint: string, node?: { span: Span }): CodexivoError {
|
|
@@ -724,7 +1347,7 @@ export class Machine {
|
|
|
724
1347
|
code,
|
|
725
1348
|
message,
|
|
726
1349
|
hint,
|
|
727
|
-
span: node?.span ?? this.currentNode?.span,
|
|
1350
|
+
span: this.visibleSpan(node?.span ?? this.currentNode?.span),
|
|
728
1351
|
callStack: this.callStack(),
|
|
729
1352
|
});
|
|
730
1353
|
}
|
|
@@ -738,7 +1361,11 @@ export class Machine {
|
|
|
738
1361
|
}
|
|
739
1362
|
|
|
740
1363
|
private unknownName(frame: Frame, name: string, node: ast.Identifier): CodexivoError {
|
|
741
|
-
const visible: string[] = [
|
|
1364
|
+
const visible: string[] = [
|
|
1365
|
+
...Object.keys(this.natives),
|
|
1366
|
+
...Object.keys(libraryProcedures),
|
|
1367
|
+
...Object.keys(libraryConstants),
|
|
1368
|
+
];
|
|
742
1369
|
for (let scope: Scope | undefined = frame.scope; scope; scope = scope.parent)
|
|
743
1370
|
visible.push(...scope.variables.keys());
|
|
744
1371
|
const suggestion = suggestName(name, visible);
|
|
@@ -752,8 +1379,42 @@ export class Machine {
|
|
|
752
1379
|
);
|
|
753
1380
|
}
|
|
754
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
|
+
|
|
755
1408
|
private undeclaredAssignment(name: string, node: ast.Identifier): CodexivoError {
|
|
756
|
-
if (
|
|
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]) {
|
|
757
1418
|
return this.runtimeError(
|
|
758
1419
|
'NOMBRE_RESERVADO_POR_BIBLIOTECA',
|
|
759
1420
|
`\`${name}\` es un procedimiento de Codexivo; no se le puede asignar un valor.`,
|
|
@@ -800,6 +1461,8 @@ export class Machine {
|
|
|
800
1461
|
|
|
801
1462
|
private emit(event: DistributiveOmit<MachineEvent, 'operations' | 'depth'>): void {
|
|
802
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;
|
|
803
1466
|
this.onEvent({ ...event, operations: this.operations, depth: this.frames.length } as MachineEvent);
|
|
804
1467
|
}
|
|
805
1468
|
}
|
|
@@ -811,20 +1474,31 @@ const splitLines = (text: string): string[] => text.replace(/\r\n/g, '\n').repla
|
|
|
811
1474
|
const formatCount = (count: number): string => count.toLocaleString('es-MX').replace(/,/g, ' ');
|
|
812
1475
|
|
|
813
1476
|
const describeCallee = (node: ast.Call): string => {
|
|
814
|
-
|
|
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
|
+
}
|
|
815
1487
|
return 'esto';
|
|
816
1488
|
};
|
|
817
1489
|
|
|
818
1490
|
const conditionHint = (value: Value): string | undefined => {
|
|
819
1491
|
switch (typeOf(value)) {
|
|
820
1492
|
case 'número':
|
|
821
|
-
return `¿Quisiste comparar? Por ejemplo: \`x
|
|
1493
|
+
return `¿Quisiste comparar? Por ejemplo: \`x es_distinto_de 0\` o \`x es_mayor_que 0\`.`;
|
|
822
1494
|
case 'texto':
|
|
823
|
-
return '¿Quisiste comparar? Por ejemplo: `texto
|
|
1495
|
+
return '¿Quisiste comparar? Por ejemplo: `texto es_distinto_de ""`.';
|
|
824
1496
|
case 'lista':
|
|
825
|
-
return '¿Quisiste decir `longitud(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)`?';
|
|
826
1500
|
case 'nulo':
|
|
827
|
-
return '¿Quisiste decir `x
|
|
1501
|
+
return '¿Quisiste decir `x es_distinto_de nulo`?';
|
|
828
1502
|
default:
|
|
829
1503
|
return undefined;
|
|
830
1504
|
}
|