codexivo 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +158 -2
- package/bin/codexivo.exe +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +20 -0
- package/docs/README.md +25 -0
- package/docs/arquitectura.md +92 -0
- package/docs/especificacion-v1.md +595 -0
- package/docs/guia-aprender-desde-el-codigo.md +91 -0
- package/docs/lenguaje.md +162 -0
- package/docs/recetario.md +159 -0
- package/docs/repl.md +64 -0
- package/package.json +75 -4
- package/postinstall.mjs +38 -0
- package/src/engine.ts +97 -0
- package/src/errors.ts +56 -0
- package/src/index.ts +29 -0
- package/src/runtime/builtins.ts +133 -0
- package/src/runtime/compiler.ts +252 -0
- package/src/runtime/machine.ts +847 -0
- package/src/runtime/suggest.ts +33 -0
- package/src/runtime/values.ts +140 -0
- package/src/syntax/ast.ts +230 -0
- package/src/syntax/lexer.ts +232 -0
- package/src/syntax/parser.ts +666 -0
- package/src/syntax/token.ts +167 -0
|
@@ -0,0 +1,847 @@
|
|
|
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 { natives as defaultNatives } from './builtins';
|
|
6
|
+
import type { Chunk, DecisionLabel, Instruction } from './compiler';
|
|
7
|
+
import { describeType, equals, isCallable, isList, repr, Scope, typeOf, WAIT_FOR_INPUT } from './values';
|
|
8
|
+
import type { ListValue, NativeContext, NativeValue, ProcedureValue, Value } from './values';
|
|
9
|
+
import { suggestName } from './suggest';
|
|
10
|
+
|
|
11
|
+
export type MachineStatus = 'lista' | 'pausada' | 'esperando_entrada' | 'terminada' | 'error';
|
|
12
|
+
|
|
13
|
+
export interface MachineOptions {
|
|
14
|
+
/** Máximo de operaciones antes de detener el programa. Por defecto 10 000 000. */
|
|
15
|
+
maxOperations?: number;
|
|
16
|
+
/** Máximo de llamadas anidadas. Por defecto 10 000. */
|
|
17
|
+
maxCallDepth?: number;
|
|
18
|
+
/** Máximo de caracteres de salida. Por defecto 1 000 000. */
|
|
19
|
+
maxOutput?: number;
|
|
20
|
+
/** Líneas de entrada para `leer()`. */
|
|
21
|
+
input?: string | string[];
|
|
22
|
+
/** Si es `true`, `leer()` sin entrada pausa la máquina en lugar de regresar `nulo`. */
|
|
23
|
+
interactiveInput?: boolean;
|
|
24
|
+
/** Procedimientos nativos adicionales (por ejemplo, los del modo mundo). */
|
|
25
|
+
natives?: Record<string, NativeValue>;
|
|
26
|
+
/** Líneas donde `run()` se detiene antes de ejecutar la sentencia. */
|
|
27
|
+
breakpoints?: number[];
|
|
28
|
+
/** Alcance global compartido entre ejecuciones (lo usa el REPL). */
|
|
29
|
+
globals?: Scope;
|
|
30
|
+
/** Permite volver a declarar una variable existente (útil en el REPL). */
|
|
31
|
+
allowRedeclare?: boolean;
|
|
32
|
+
/** Recibe cada evento de la ejecución. Activarlo hace la ejecución más lenta. */
|
|
33
|
+
onEvent?: (event: MachineEvent) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── eventos y fotografías ────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
export type SerializedValue =
|
|
39
|
+
| { type: 'número'; value: number; repr: string }
|
|
40
|
+
| { type: 'texto'; value: string; repr: string }
|
|
41
|
+
| { type: 'lógico'; value: boolean; repr: string }
|
|
42
|
+
| { type: 'nulo'; value: null; repr: string }
|
|
43
|
+
| { type: 'lista'; ref: number; length: number; repr: string }
|
|
44
|
+
| { type: 'procedimiento'; name?: string; repr: string };
|
|
45
|
+
|
|
46
|
+
export const serialize = (value: Value): SerializedValue => {
|
|
47
|
+
const text = repr(value);
|
|
48
|
+
if (value === null) return { type: 'nulo', value: null, repr: text };
|
|
49
|
+
switch (typeof value) {
|
|
50
|
+
case 'number':
|
|
51
|
+
return { type: 'número', value, repr: text };
|
|
52
|
+
case 'string':
|
|
53
|
+
return { type: 'texto', value, repr: text };
|
|
54
|
+
case 'boolean':
|
|
55
|
+
return { type: 'lógico', value, repr: text };
|
|
56
|
+
}
|
|
57
|
+
if (value.kind === 'lista') return { type: 'lista', ref: value.id, length: value.items.length, repr: text };
|
|
58
|
+
return { type: 'procedimiento', name: value.name, repr: text };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
interface EventBase {
|
|
62
|
+
/** Operaciones ejecutadas hasta este evento. */
|
|
63
|
+
operations: number;
|
|
64
|
+
/** Profundidad de llamadas: 1 en el programa principal. */
|
|
65
|
+
depth: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type MachineEvent = EventBase &
|
|
69
|
+
(
|
|
70
|
+
| { type: 'paso'; nodeId: number; nodeKind: ast.Statement['kind']; span: Span }
|
|
71
|
+
| { type: 'declara'; name: string; value: SerializedValue; nodeId: number; span: Span }
|
|
72
|
+
| { type: 'asigna'; name: string; before: SerializedValue; after: SerializedValue; nodeId: number; span: Span }
|
|
73
|
+
| { type: 'decide'; label: DecisionLabel; value: boolean; nodeId: number; ownerId: number; span: Span }
|
|
74
|
+
| {
|
|
75
|
+
type: 'opera';
|
|
76
|
+
operator: string;
|
|
77
|
+
operands: SerializedValue[];
|
|
78
|
+
result: SerializedValue;
|
|
79
|
+
nodeId: number;
|
|
80
|
+
span: Span;
|
|
81
|
+
}
|
|
82
|
+
| { type: 'llama'; name: string; args: SerializedValue[]; nodeId: number; span: Span }
|
|
83
|
+
| { type: 'regresa'; name: string; value: SerializedValue }
|
|
84
|
+
| { type: 'imprime'; text: string }
|
|
85
|
+
| { type: 'lee'; text: string | null }
|
|
86
|
+
| { type: 'error'; error: ErrorInfo }
|
|
87
|
+
| { type: 'fin'; result: SerializedValue }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
export interface FrameSnapshot {
|
|
91
|
+
name: string;
|
|
92
|
+
callSpan?: Span;
|
|
93
|
+
/** Alcances del procedimiento, del más externo al más interno. */
|
|
94
|
+
scopes: Array<Record<string, SerializedValue>>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Snapshot {
|
|
98
|
+
status: MachineStatus;
|
|
99
|
+
operations: number;
|
|
100
|
+
/** Nodo donde está detenida la máquina. */
|
|
101
|
+
nodeId?: number;
|
|
102
|
+
span?: Span;
|
|
103
|
+
/** Llamadas activas, del programa principal a la más reciente. */
|
|
104
|
+
frames: FrameSnapshot[];
|
|
105
|
+
/** Contenido de las listas que aparecen en `frames`, por `ref`. */
|
|
106
|
+
heap: Record<number, SerializedValue[]>;
|
|
107
|
+
output: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── máquina ──────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
interface Frame {
|
|
113
|
+
chunk: Chunk;
|
|
114
|
+
pc: number;
|
|
115
|
+
scope: Scope;
|
|
116
|
+
/** Primer alcance del marco; el que contiene a los parámetros. */
|
|
117
|
+
root: Scope;
|
|
118
|
+
/** Posición en la pila de valores donde empieza este marco (el procedimiento llamado está justo antes). */
|
|
119
|
+
stackBase: number;
|
|
120
|
+
name: string;
|
|
121
|
+
callNode?: ast.Call;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const DEFAULTS = {
|
|
125
|
+
maxOperations: 10_000_000,
|
|
126
|
+
maxCallDepth: 10_000,
|
|
127
|
+
maxOutput: 1_000_000,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const NO_COUNT = new Set<Instruction['op']>(['step', 'enterScope', 'exitScope', 'jump', 'halt']);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Ejecuta un programa compilado. Tiene su propia pila de llamadas, así que la recursión no depende
|
|
134
|
+
* de la pila de JavaScript, y se puede pausar en cualquier sentencia o decisión.
|
|
135
|
+
*/
|
|
136
|
+
export class Machine {
|
|
137
|
+
private readonly frames: Frame[] = [];
|
|
138
|
+
private readonly stack: Value[] = [];
|
|
139
|
+
private readonly natives: Record<string, NativeValue>;
|
|
140
|
+
private readonly inputQueue: string[];
|
|
141
|
+
private readonly breakpoints: Set<number>;
|
|
142
|
+
private readonly limits: typeof DEFAULTS;
|
|
143
|
+
private readonly onEvent?: (event: MachineEvent) => void;
|
|
144
|
+
private readonly outputParts: string[] = [];
|
|
145
|
+
private outputLength = 0;
|
|
146
|
+
private nextId = 1;
|
|
147
|
+
private lastValue: Value = null;
|
|
148
|
+
private inputClosed = false;
|
|
149
|
+
private currentNode?: ast.Node;
|
|
150
|
+
|
|
151
|
+
status: MachineStatus = 'lista';
|
|
152
|
+
operations = 0;
|
|
153
|
+
result: Value = null;
|
|
154
|
+
error?: CodexivoError;
|
|
155
|
+
|
|
156
|
+
constructor(
|
|
157
|
+
program: Chunk,
|
|
158
|
+
private readonly options: MachineOptions = {},
|
|
159
|
+
) {
|
|
160
|
+
this.natives = { ...defaultNatives, ...options.natives };
|
|
161
|
+
this.limits = {
|
|
162
|
+
maxOperations: options.maxOperations ?? DEFAULTS.maxOperations,
|
|
163
|
+
maxCallDepth: options.maxCallDepth ?? DEFAULTS.maxCallDepth,
|
|
164
|
+
maxOutput: options.maxOutput ?? DEFAULTS.maxOutput,
|
|
165
|
+
};
|
|
166
|
+
this.inputQueue = typeof options.input === 'string' ? splitLines(options.input) : [...(options.input ?? [])];
|
|
167
|
+
this.breakpoints = new Set(options.breakpoints ?? []);
|
|
168
|
+
this.onEvent = options.onEvent;
|
|
169
|
+
const globals = options.globals ?? new Scope();
|
|
170
|
+
this.frames.push({ chunk: program, pc: 0, scope: globals, root: globals, stackBase: 0, name: 'programa' });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Todo lo que el programa ha impreso. */
|
|
174
|
+
get output(): string {
|
|
175
|
+
return this.outputParts.join('');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get done(): boolean {
|
|
179
|
+
return this.status === 'terminada' || this.status === 'error';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Ejecuta hasta terminar, fallar, esperar entrada, llegar a un punto de interrupción o agotar `budget` operaciones. */
|
|
183
|
+
run(budget = Infinity): MachineStatus {
|
|
184
|
+
return this.execute('run', budget);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Ejecuta hasta la siguiente sentencia o decisión. */
|
|
188
|
+
step(): MachineStatus {
|
|
189
|
+
return this.execute('step', Infinity);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Entrega una línea a un `leer()` que está esperando. */
|
|
193
|
+
provideInput(line: string): void {
|
|
194
|
+
this.inputQueue.push(...splitLines(line));
|
|
195
|
+
if (this.status === 'esperando_entrada') this.status = 'pausada';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Indica que no habrá más entrada: desde ahora `leer()` regresa `nulo` en lugar de esperar. */
|
|
199
|
+
closeInput(): void {
|
|
200
|
+
this.inputClosed = true;
|
|
201
|
+
if (this.status === 'esperando_entrada') this.status = 'pausada';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
snapshot(): Snapshot {
|
|
205
|
+
const heap: Record<number, SerializedValue[]> = {};
|
|
206
|
+
const collect = (value: Value) => {
|
|
207
|
+
if (!isList(value) || heap[value.id]) return;
|
|
208
|
+
heap[value.id] = value.items.map(serialize);
|
|
209
|
+
value.items.forEach(collect);
|
|
210
|
+
};
|
|
211
|
+
const frames = this.frames.map(frame => {
|
|
212
|
+
const scopes: Array<Record<string, SerializedValue>> = [];
|
|
213
|
+
let scope: Scope | undefined = frame.scope;
|
|
214
|
+
while (scope) {
|
|
215
|
+
const variables: Record<string, SerializedValue> = {};
|
|
216
|
+
for (const [name, value] of scope.variables) {
|
|
217
|
+
variables[name] = serialize(value);
|
|
218
|
+
collect(value);
|
|
219
|
+
}
|
|
220
|
+
scopes.unshift(variables);
|
|
221
|
+
if (scope === frame.root) break;
|
|
222
|
+
scope = scope.parent;
|
|
223
|
+
}
|
|
224
|
+
return { name: frame.name, callSpan: frame.callNode?.span, scopes };
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
status: this.status,
|
|
228
|
+
operations: this.operations,
|
|
229
|
+
nodeId: this.currentNode?.id,
|
|
230
|
+
span: this.currentNode?.span,
|
|
231
|
+
frames,
|
|
232
|
+
heap,
|
|
233
|
+
output: this.output,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── ciclo principal ────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
private execute(mode: 'run' | 'step', budget: number): MachineStatus {
|
|
240
|
+
if (this.done || this.status === 'esperando_entrada') return this.status;
|
|
241
|
+
const stopAt = this.operations + budget;
|
|
242
|
+
let executed = 0;
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
while (true) {
|
|
246
|
+
const frame = this.frames[this.frames.length - 1]!;
|
|
247
|
+
const instruction = frame.chunk.code[frame.pc]!;
|
|
248
|
+
|
|
249
|
+
if (!NO_COUNT.has(instruction.op)) {
|
|
250
|
+
if (this.operations >= this.limits.maxOperations) {
|
|
251
|
+
throw this.limitError(
|
|
252
|
+
'DEMASIADAS_OPERACIONES',
|
|
253
|
+
`El programa superó el límite de ${formatCount(this.limits.maxOperations)} operaciones.`,
|
|
254
|
+
'¿Hay un bucle que nunca termina? Revisa que su condición cambie en algún momento.',
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (this.operations >= stopAt) {
|
|
258
|
+
this.status = 'pausada';
|
|
259
|
+
return this.status;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Los puntos de interrupción detienen `run()` antes de la sentencia.
|
|
264
|
+
if (
|
|
265
|
+
instruction.op === 'step' &&
|
|
266
|
+
mode === 'run' &&
|
|
267
|
+
executed > 0 &&
|
|
268
|
+
this.breakpoints.has(instruction.node.span.start.line)
|
|
269
|
+
) {
|
|
270
|
+
this.status = 'pausada';
|
|
271
|
+
return this.status;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const pause = this.exec(frame, instruction);
|
|
275
|
+
executed += 1;
|
|
276
|
+
// `exec` puede cambiar el estado.
|
|
277
|
+
const status = this.status as MachineStatus;
|
|
278
|
+
if (status === 'esperando_entrada' || status === 'terminada') return status;
|
|
279
|
+
if (mode === 'step' && pause) {
|
|
280
|
+
this.status = 'pausada';
|
|
281
|
+
return this.status;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (!(error instanceof CodexivoError)) throw error;
|
|
286
|
+
this.error = error;
|
|
287
|
+
this.status = 'error';
|
|
288
|
+
this.emit({ type: 'error', error: error.toJSON() });
|
|
289
|
+
return this.status;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Ejecuta una instrucción. Regresa `true` si es un punto de pausa para `step()`. */
|
|
294
|
+
private exec(frame: Frame, instruction: Instruction): boolean {
|
|
295
|
+
frame.pc += 1;
|
|
296
|
+
if (!NO_COUNT.has(instruction.op)) this.operations += 1;
|
|
297
|
+
|
|
298
|
+
switch (instruction.op) {
|
|
299
|
+
case 'step':
|
|
300
|
+
this.currentNode = instruction.node;
|
|
301
|
+
this.emit({
|
|
302
|
+
type: 'paso',
|
|
303
|
+
nodeId: instruction.node.id,
|
|
304
|
+
nodeKind: instruction.node.kind,
|
|
305
|
+
span: instruction.node.span,
|
|
306
|
+
});
|
|
307
|
+
return true;
|
|
308
|
+
|
|
309
|
+
case 'const':
|
|
310
|
+
this.stack.push(instruction.value);
|
|
311
|
+
return false;
|
|
312
|
+
|
|
313
|
+
case 'list': {
|
|
314
|
+
const items = this.stack.splice(this.stack.length - instruction.count, instruction.count);
|
|
315
|
+
this.stack.push(this.newList(items));
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
case 'load': {
|
|
320
|
+
const scope = frame.scope.lookup(instruction.name);
|
|
321
|
+
if (scope) {
|
|
322
|
+
this.stack.push(scope.variables.get(instruction.name)!);
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
const native = this.natives[instruction.name];
|
|
326
|
+
if (native && !instruction.forUpdate) {
|
|
327
|
+
this.stack.push(native);
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
if (instruction.forUpdate) throw this.undeclaredAssignment(instruction.name, instruction.node);
|
|
331
|
+
throw this.unknownName(frame, instruction.name, instruction.node);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
case 'declare': {
|
|
335
|
+
const value = this.stack.pop()!;
|
|
336
|
+
if (frame.scope.variables.has(instruction.name) && !this.options.allowRedeclare) {
|
|
337
|
+
throw this.runtimeError(
|
|
338
|
+
'VARIABLE_REPETIDA',
|
|
339
|
+
`La variable \`${instruction.name}\` ya existe en este bloque.`,
|
|
340
|
+
instruction.node.name,
|
|
341
|
+
`Para cambiar su valor escribe \`${instruction.name} = …\`, sin \`variable\`.`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
frame.scope.variables.set(instruction.name, value);
|
|
345
|
+
this.emit({
|
|
346
|
+
type: 'declara',
|
|
347
|
+
name: instruction.name,
|
|
348
|
+
value: serialize(value),
|
|
349
|
+
nodeId: instruction.node.id,
|
|
350
|
+
span: instruction.node.span,
|
|
351
|
+
});
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
case 'store': {
|
|
356
|
+
const value = this.stack.pop()!;
|
|
357
|
+
const scope = frame.scope.lookup(instruction.name);
|
|
358
|
+
if (!scope) throw this.undeclaredAssignment(instruction.name, instruction.node.target);
|
|
359
|
+
const before = scope.variables.get(instruction.name)!;
|
|
360
|
+
scope.variables.set(instruction.name, value);
|
|
361
|
+
if (this.onEvent) {
|
|
362
|
+
this.emit({
|
|
363
|
+
type: 'asigna',
|
|
364
|
+
name: instruction.name,
|
|
365
|
+
before: serialize(before),
|
|
366
|
+
after: serialize(value),
|
|
367
|
+
nodeId: instruction.node.id,
|
|
368
|
+
span: instruction.node.span,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
case 'result': {
|
|
375
|
+
const value = this.stack.pop()!;
|
|
376
|
+
if (this.frames.length === 1) this.lastValue = value;
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
case 'unary': {
|
|
381
|
+
const operand = this.stack.pop()!;
|
|
382
|
+
const result = this.unary(instruction.operator, operand, instruction.node);
|
|
383
|
+
this.stack.push(result);
|
|
384
|
+
if (this.onEvent) this.emitOperation(instruction.operator, [operand], result, instruction.node);
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
case 'binary': {
|
|
389
|
+
const right = this.stack.pop()!;
|
|
390
|
+
const left = this.stack.pop()!;
|
|
391
|
+
const result = this.binary(instruction.operator, left, right, instruction.node);
|
|
392
|
+
this.stack.push(result);
|
|
393
|
+
if (this.onEvent) this.emitOperation(instruction.operator, [left, right], result, instruction.node);
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
case 'index': {
|
|
398
|
+
const index = this.stack.pop()!;
|
|
399
|
+
const object = this.stack.pop()!;
|
|
400
|
+
this.stack.push(this.index(object, index, instruction.node));
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
case 'closure': {
|
|
405
|
+
const procedure: ProcedureValue = {
|
|
406
|
+
kind: 'procedimiento',
|
|
407
|
+
id: this.nextId++,
|
|
408
|
+
name: instruction.node.name,
|
|
409
|
+
chunk: instruction.chunk,
|
|
410
|
+
closure: frame.scope,
|
|
411
|
+
};
|
|
412
|
+
this.stack.push(procedure);
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
case 'call':
|
|
417
|
+
return this.call(frame, instruction.argc, instruction.node);
|
|
418
|
+
|
|
419
|
+
case 'return': {
|
|
420
|
+
const value = instruction.hasValue ? this.stack.pop()! : null;
|
|
421
|
+
if (this.frames.length === 1) {
|
|
422
|
+
this.finish(value);
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
this.emit({ type: 'regresa', name: frame.name, value: serialize(value) });
|
|
426
|
+
this.frames.pop();
|
|
427
|
+
this.stack.length = frame.stackBase - 1;
|
|
428
|
+
this.stack.push(value);
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
case 'jump':
|
|
433
|
+
frame.pc = instruction.target;
|
|
434
|
+
return false;
|
|
435
|
+
|
|
436
|
+
case 'branch': {
|
|
437
|
+
const condition = this.stack.pop()!;
|
|
438
|
+
if (typeof condition !== 'boolean')
|
|
439
|
+
throw this.nonBooleanCondition(instruction.label, condition, instruction.node);
|
|
440
|
+
this.currentNode = instruction.node;
|
|
441
|
+
this.emit({
|
|
442
|
+
type: 'decide',
|
|
443
|
+
label: instruction.label,
|
|
444
|
+
value: condition,
|
|
445
|
+
nodeId: instruction.node.id,
|
|
446
|
+
ownerId: instruction.owner.id,
|
|
447
|
+
span: instruction.node.span,
|
|
448
|
+
});
|
|
449
|
+
if (condition === instruction.jumpWhen) frame.pc = instruction.target;
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
case 'shortCircuit': {
|
|
454
|
+
const left = this.stack[this.stack.length - 1]!;
|
|
455
|
+
if (typeof left !== 'boolean') throw this.nonBooleanLogical(instruction.node, left, instruction.node.left);
|
|
456
|
+
const decided = instruction.node.operator === 'y' ? left === false : left === true;
|
|
457
|
+
if (decided) {
|
|
458
|
+
frame.pc = instruction.target;
|
|
459
|
+
if (this.onEvent) this.emitOperation(instruction.node.operator, [left], left, instruction.node);
|
|
460
|
+
} else {
|
|
461
|
+
this.stack.pop();
|
|
462
|
+
}
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
case 'checkLogical': {
|
|
467
|
+
const right = this.stack[this.stack.length - 1]!;
|
|
468
|
+
if (typeof right !== 'boolean') throw this.nonBooleanLogical(instruction.node, right, instruction.node.right);
|
|
469
|
+
if (this.onEvent) {
|
|
470
|
+
const left = instruction.node.operator === 'y';
|
|
471
|
+
this.emitOperation(instruction.node.operator, [left, right], right, instruction.node);
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
case 'enterScope':
|
|
477
|
+
frame.scope = new Scope(frame.scope);
|
|
478
|
+
return false;
|
|
479
|
+
|
|
480
|
+
case 'exitScope':
|
|
481
|
+
frame.scope = frame.scope.parent ?? frame.scope;
|
|
482
|
+
return false;
|
|
483
|
+
|
|
484
|
+
case 'halt':
|
|
485
|
+
this.finish(this.lastValue);
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private finish(value: Value): void {
|
|
491
|
+
this.result = value;
|
|
492
|
+
this.status = 'terminada';
|
|
493
|
+
this.emit({ type: 'fin', result: serialize(value) });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ── llamadas ───────────────────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
private call(frame: Frame, argc: number, node: ast.Call): boolean {
|
|
499
|
+
const calleeIndex = this.stack.length - argc - 1;
|
|
500
|
+
const callee = this.stack[calleeIndex]!;
|
|
501
|
+
const name =
|
|
502
|
+
callee !== null && typeof callee === 'object' && 'name' in callee && callee.name
|
|
503
|
+
? callee.name
|
|
504
|
+
: describeCallee(node);
|
|
505
|
+
|
|
506
|
+
if (!isCallable(callee)) {
|
|
507
|
+
throw this.runtimeError(
|
|
508
|
+
'NO_ES_PROCEDIMIENTO',
|
|
509
|
+
`\`${describeCallee(node)}\` no es un procedimiento, es ${describeType(typeOf(callee))}.`,
|
|
510
|
+
node.callee,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const args = this.stack.slice(calleeIndex + 1);
|
|
515
|
+
const expected = callee.kind === 'nativo' ? callee.arity : callee.chunk.params.length;
|
|
516
|
+
checkArity(expected, argc, name, (message, hint) =>
|
|
517
|
+
this.runtimeError('CANTIDAD_DE_ARGUMENTOS', message, node, hint),
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
if (callee.kind === 'nativo') {
|
|
521
|
+
const result = callee.call(args, this.nativeContext(node));
|
|
522
|
+
if (result === WAIT_FOR_INPUT) {
|
|
523
|
+
// Repetimos la llamada cuando llegue la entrada.
|
|
524
|
+
frame.pc -= 1;
|
|
525
|
+
this.operations -= 1;
|
|
526
|
+
this.status = 'esperando_entrada';
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
this.stack.length = calleeIndex;
|
|
530
|
+
this.stack.push(result);
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// El primer marco es el programa principal, no una llamada.
|
|
535
|
+
if (this.frames.length - 1 >= this.limits.maxCallDepth) {
|
|
536
|
+
throw this.limitError(
|
|
537
|
+
'DEMASIADAS_LLAMADAS',
|
|
538
|
+
`Hay más de ${formatCount(this.limits.maxCallDepth)} llamadas anidadas.`,
|
|
539
|
+
'Si es recursión, revisa que exista un caso base que deje de llamar al procedimiento.',
|
|
540
|
+
node,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const scope = new Scope(callee.closure);
|
|
545
|
+
callee.chunk.params.forEach((param, index) => scope.variables.set(param, args[index]!));
|
|
546
|
+
this.emit({ type: 'llama', name, args: args.map(serialize), nodeId: node.id, span: node.span });
|
|
547
|
+
this.frames.push({
|
|
548
|
+
chunk: callee.chunk,
|
|
549
|
+
pc: 0,
|
|
550
|
+
scope,
|
|
551
|
+
root: scope,
|
|
552
|
+
stackBase: calleeIndex + 1,
|
|
553
|
+
name,
|
|
554
|
+
callNode: node,
|
|
555
|
+
});
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private nativeContext(node: ast.Call): NativeContext {
|
|
560
|
+
return {
|
|
561
|
+
write: text => {
|
|
562
|
+
this.outputLength += text.length;
|
|
563
|
+
if (this.outputLength > this.limits.maxOutput) {
|
|
564
|
+
throw this.limitError(
|
|
565
|
+
'DEMASIADA_SALIDA',
|
|
566
|
+
`El programa imprimió más de ${formatCount(this.limits.maxOutput)} caracteres.`,
|
|
567
|
+
'¿Hay un `imprimir` dentro de un bucle que nunca termina?',
|
|
568
|
+
node,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
this.outputParts.push(text);
|
|
572
|
+
this.emit({ type: 'imprime', text });
|
|
573
|
+
},
|
|
574
|
+
readLine: () => {
|
|
575
|
+
if (this.inputQueue.length > 0) {
|
|
576
|
+
const line = this.inputQueue.shift()!;
|
|
577
|
+
this.emit({ type: 'lee', text: line });
|
|
578
|
+
return line;
|
|
579
|
+
}
|
|
580
|
+
if (this.options.interactiveInput && !this.inputClosed) return WAIT_FOR_INPUT;
|
|
581
|
+
this.emit({ type: 'lee', text: null });
|
|
582
|
+
return null;
|
|
583
|
+
},
|
|
584
|
+
newList: items => this.newList(items),
|
|
585
|
+
fail: (code, message, hint) => this.runtimeError(code, message, node, hint),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private newList(items: Value[]): ListValue {
|
|
590
|
+
return { kind: 'lista', id: this.nextId++, items };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ── operadores ─────────────────────────────────────────────────────────────
|
|
594
|
+
|
|
595
|
+
private unary(operator: ast.UnaryOperator, operand: Value, node: ast.Unary): Value {
|
|
596
|
+
if (operator === '-') {
|
|
597
|
+
if (typeof operand !== 'number') {
|
|
598
|
+
throw this.runtimeError(
|
|
599
|
+
'TIPO_INVÁLIDO',
|
|
600
|
+
`El signo \`-\` solo funciona con números, no con ${describeType(typeOf(operand))}.`,
|
|
601
|
+
node,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
return -operand;
|
|
605
|
+
}
|
|
606
|
+
if (typeof operand !== 'boolean') {
|
|
607
|
+
throw this.runtimeError(
|
|
608
|
+
'TIPO_INVÁLIDO',
|
|
609
|
+
`\`no\` solo funciona con \`verdadero\` o \`falso\`, pero recibió ${repr(operand)}.`,
|
|
610
|
+
node.operand,
|
|
611
|
+
conditionHint(operand),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
return !operand;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private binary(operator: ast.BinaryOperator, left: Value, right: Value, node: ast.Binary | ast.Assignment): Value {
|
|
618
|
+
switch (operator) {
|
|
619
|
+
case '==':
|
|
620
|
+
return equals(left, right);
|
|
621
|
+
case '!=':
|
|
622
|
+
return !equals(left, right);
|
|
623
|
+
case '+':
|
|
624
|
+
if (typeof left === 'number' && typeof right === 'number') return left + right;
|
|
625
|
+
if (typeof left === 'string' && typeof right === 'string') return left + right;
|
|
626
|
+
if (typeof left === 'string' || typeof right === 'string') {
|
|
627
|
+
const other = typeof left === 'string' ? right : left;
|
|
628
|
+
throw this.runtimeError(
|
|
629
|
+
'TIPOS_MEZCLADOS',
|
|
630
|
+
`\`+\` no puede unir un texto con ${describeType(typeOf(other))}.`,
|
|
631
|
+
node,
|
|
632
|
+
`Convierte el valor con \`texto(…)\`, o imprime los dos por separado: \`imprimir("Total:", ${repr(other)})\`.`,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
throw this.operandError(operator, left, right, node);
|
|
636
|
+
case '-':
|
|
637
|
+
case '*':
|
|
638
|
+
case '/': {
|
|
639
|
+
if (typeof left !== 'number' || typeof right !== 'number') throw this.operandError(operator, left, right, node);
|
|
640
|
+
if (operator === '-') return left - right;
|
|
641
|
+
if (operator === '*') return left * right;
|
|
642
|
+
if (right === 0) {
|
|
643
|
+
const divisor = node.kind === 'Binary' ? node.right : node.value;
|
|
644
|
+
throw this.runtimeError(
|
|
645
|
+
'DIVISIÓN_ENTRE_CERO',
|
|
646
|
+
'No se puede dividir entre cero.',
|
|
647
|
+
divisor,
|
|
648
|
+
'Revisa que el divisor no valga 0 antes de dividir.',
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
return left / right;
|
|
652
|
+
}
|
|
653
|
+
case '<':
|
|
654
|
+
case '>':
|
|
655
|
+
case '<=':
|
|
656
|
+
case '>=': {
|
|
657
|
+
const comparable =
|
|
658
|
+
(typeof left === 'number' && typeof right === 'number') ||
|
|
659
|
+
(typeof left === 'string' && typeof right === 'string');
|
|
660
|
+
if (!comparable) {
|
|
661
|
+
throw this.runtimeError(
|
|
662
|
+
'COMPARACIÓN_INVÁLIDA',
|
|
663
|
+
`No se puede comparar ${describeType(typeOf(left))} con ${describeType(typeOf(right))} usando \`${operator}\`.`,
|
|
664
|
+
node,
|
|
665
|
+
'`<`, `>`, `<=` y `>=` comparan dos números o dos textos.',
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
const a = left as number | string;
|
|
669
|
+
const b = right as number | string;
|
|
670
|
+
if (operator === '<') return a < b;
|
|
671
|
+
if (operator === '>') return a > b;
|
|
672
|
+
if (operator === '<=') return a <= b;
|
|
673
|
+
return a >= b;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private index(object: Value, index: Value, node: ast.Index): Value {
|
|
679
|
+
if (!isList(object) && typeof object !== 'string') {
|
|
680
|
+
throw this.runtimeError(
|
|
681
|
+
'ÍNDICE_NO_SOPORTADO',
|
|
682
|
+
`Solo se puede usar \`[ ]\` con listas y textos, no con ${describeType(typeOf(object))}.`,
|
|
683
|
+
node.object,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (typeof index !== 'number' || !Number.isInteger(index)) {
|
|
687
|
+
throw this.runtimeError(
|
|
688
|
+
'ÍNDICE_INVÁLIDO',
|
|
689
|
+
`El índice tiene que ser un número entero, pero es ${repr(index)}.`,
|
|
690
|
+
node.index,
|
|
691
|
+
typeof index === 'number' ? 'Redondea el índice con `redondear(…)`.' : undefined,
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
const length = isList(object) ? object.items.length : object.length;
|
|
695
|
+
const position = index < 0 ? length + index : index;
|
|
696
|
+
if (position < 0 || position >= length) {
|
|
697
|
+
const what = isList(object) ? 'la lista' : 'el texto';
|
|
698
|
+
const unit = isList(object) ? 'elemento' : 'carácter';
|
|
699
|
+
throw this.runtimeError(
|
|
700
|
+
'ÍNDICE_FUERA_DE_RANGO',
|
|
701
|
+
`El índice ${index} está fuera de ${what}, que tiene ${length} ${length === 1 ? unit : unit === 'carácter' ? 'caracteres' : 'elementos'}.`,
|
|
702
|
+
node.index,
|
|
703
|
+
length === 0
|
|
704
|
+
? `${what[0]!.toUpperCase()}${what.slice(1)} está vacía.`
|
|
705
|
+
: `Los índices válidos van de 0 a ${length - 1} (o de -1 a -${length} desde el final).`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
return isList(object) ? object.items[position]! : object[position]!;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ── errores ────────────────────────────────────────────────────────────────
|
|
712
|
+
|
|
713
|
+
private callStack(): string[] {
|
|
714
|
+
return this.frames.map(frame => frame.name);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private runtimeError(code: string, message: string, node: { span: Span }, hint?: string): CodexivoError {
|
|
718
|
+
return new CodexivoError({ kind: 'ejecución', code, message, hint, span: node.span, callStack: this.callStack() });
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private limitError(code: string, message: string, hint: string, node?: { span: Span }): CodexivoError {
|
|
722
|
+
return new CodexivoError({
|
|
723
|
+
kind: 'límite',
|
|
724
|
+
code,
|
|
725
|
+
message,
|
|
726
|
+
hint,
|
|
727
|
+
span: node?.span ?? this.currentNode?.span,
|
|
728
|
+
callStack: this.callStack(),
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private operandError(operator: string, left: Value, right: Value, node: { span: Span }): CodexivoError {
|
|
733
|
+
return this.runtimeError(
|
|
734
|
+
'TIPO_INVÁLIDO',
|
|
735
|
+
`\`${operator}\` no funciona entre ${describeType(typeOf(left))} y ${describeType(typeOf(right))}.`,
|
|
736
|
+
node,
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
private unknownName(frame: Frame, name: string, node: ast.Identifier): CodexivoError {
|
|
741
|
+
const visible: string[] = [...Object.keys(this.natives)];
|
|
742
|
+
for (let scope: Scope | undefined = frame.scope; scope; scope = scope.parent)
|
|
743
|
+
visible.push(...scope.variables.keys());
|
|
744
|
+
const suggestion = suggestName(name, visible);
|
|
745
|
+
return this.runtimeError(
|
|
746
|
+
'NOMBRE_DESCONOCIDO',
|
|
747
|
+
`No existe nada llamado \`${name}\`.`,
|
|
748
|
+
node,
|
|
749
|
+
suggestion
|
|
750
|
+
? `¿Quisiste decir \`${suggestion}\`?`
|
|
751
|
+
: `Si es una variable nueva, declárala con \`variable ${name} = …\`.`,
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private undeclaredAssignment(name: string, node: ast.Identifier): CodexivoError {
|
|
756
|
+
if (this.natives[name]) {
|
|
757
|
+
return this.runtimeError(
|
|
758
|
+
'NOMBRE_RESERVADO_POR_BIBLIOTECA',
|
|
759
|
+
`\`${name}\` es un procedimiento de Codexivo; no se le puede asignar un valor.`,
|
|
760
|
+
node,
|
|
761
|
+
`Si quieres tu propia variable con ese nombre, declárala: \`variable ${name} = …\`.`,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
return this.runtimeError(
|
|
765
|
+
'VARIABLE_NO_DECLARADA',
|
|
766
|
+
`La variable \`${name}\` no existe.`,
|
|
767
|
+
node,
|
|
768
|
+
`¿Olvidaste declararla? Escribe \`variable ${name} = …\` la primera vez.`,
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private nonBooleanCondition(label: DecisionLabel, value: Value, node: ast.Expression): CodexivoError {
|
|
773
|
+
return this.runtimeError(
|
|
774
|
+
'CONDICIÓN_NO_LÓGICA',
|
|
775
|
+
`La condición de \`${label}\` tiene que ser \`verdadero\` o \`falso\`, pero vale ${repr(value)}.`,
|
|
776
|
+
node,
|
|
777
|
+
conditionHint(value),
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private nonBooleanLogical(node: ast.Logical, value: Value, side: ast.Expression): CodexivoError {
|
|
782
|
+
return this.runtimeError(
|
|
783
|
+
'OPERANDO_NO_LÓGICO',
|
|
784
|
+
`\`${node.operator}\` une valores \`verdadero\` o \`falso\`, pero recibió ${repr(value)}.`,
|
|
785
|
+
side,
|
|
786
|
+
conditionHint(value),
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private emitOperation(operator: string, operands: Value[], result: Value, node: ast.Node): void {
|
|
791
|
+
this.emit({
|
|
792
|
+
type: 'opera',
|
|
793
|
+
operator,
|
|
794
|
+
operands: operands.map(serialize),
|
|
795
|
+
result: serialize(result),
|
|
796
|
+
nodeId: node.id,
|
|
797
|
+
span: node.span,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
private emit(event: DistributiveOmit<MachineEvent, 'operations' | 'depth'>): void {
|
|
802
|
+
if (!this.onEvent) return;
|
|
803
|
+
this.onEvent({ ...event, operations: this.operations, depth: this.frames.length } as MachineEvent);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
808
|
+
|
|
809
|
+
const splitLines = (text: string): string[] => text.replace(/\r\n/g, '\n').replace(/\n$/, '').split('\n');
|
|
810
|
+
|
|
811
|
+
const formatCount = (count: number): string => count.toLocaleString('es-MX').replace(/,/g, ' ');
|
|
812
|
+
|
|
813
|
+
const describeCallee = (node: ast.Call): string => {
|
|
814
|
+
if (node.callee.kind === 'Identifier') return node.callee.name;
|
|
815
|
+
return 'esto';
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const conditionHint = (value: Value): string | undefined => {
|
|
819
|
+
switch (typeOf(value)) {
|
|
820
|
+
case 'número':
|
|
821
|
+
return `¿Quisiste comparar? Por ejemplo: \`x != 0\` o \`x > 0\`.`;
|
|
822
|
+
case 'texto':
|
|
823
|
+
return '¿Quisiste comparar? Por ejemplo: `texto != ""`.';
|
|
824
|
+
case 'lista':
|
|
825
|
+
return '¿Quisiste decir `longitud(lista) > 0`?';
|
|
826
|
+
case 'nulo':
|
|
827
|
+
return '¿Quisiste decir `x != nulo`?';
|
|
828
|
+
default:
|
|
829
|
+
return undefined;
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
const checkArity = (
|
|
834
|
+
expected: number | [number, number],
|
|
835
|
+
received: number,
|
|
836
|
+
name: string,
|
|
837
|
+
fail: (message: string, hint?: string) => CodexivoError,
|
|
838
|
+
): void => {
|
|
839
|
+
const [min, max] = typeof expected === 'number' ? [expected, expected] : expected;
|
|
840
|
+
if (received >= min && received <= max) return;
|
|
841
|
+
const plural = (count: number) => (count === 1 ? '1 valor' : `${count} valores`);
|
|
842
|
+
let wanted: string;
|
|
843
|
+
if (min === max) wanted = plural(min);
|
|
844
|
+
else if (max === Infinity) wanted = `al menos ${plural(min)}`;
|
|
845
|
+
else wanted = `entre ${min} y ${plural(max)}`;
|
|
846
|
+
throw fail(`\`${name}\` espera ${wanted}, pero recibió ${received}.`);
|
|
847
|
+
};
|