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
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { evaluarReto } from './challenge';
|
|
2
|
+
import type { Challenge, ChallengeResult } from './challenge';
|
|
3
|
+
import type { ErrorInfo } from './errors';
|
|
4
|
+
import { Machine, serialize } from './runtime/machine';
|
|
5
|
+
import type { MachineEvent, MachineOptions, MachineStatus, SerializedValue, Snapshot } from './runtime/machine';
|
|
6
|
+
import { compile } from './runtime/compiler';
|
|
7
|
+
import { parse } from './syntax/parser';
|
|
8
|
+
import type * as ast from './syntax/ast';
|
|
9
|
+
import type { Token } from './syntax/token';
|
|
10
|
+
import { createWorld } from './world';
|
|
11
|
+
import type { WorldDefinition } from './world';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Protocolo del sandbox: los mensajes entre la página y el worker que ejecuta el código. La lógica vive aquí,
|
|
15
|
+
* sin depender de `postMessage`, para poder probarla; `worker.ts` la conecta con el worker y `sandbox.ts` es
|
|
16
|
+
* el cliente tipado para la página.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Qué eventos llegan en cada tanda. */
|
|
20
|
+
export type EventDetail = 'nada' | 'pasos' | 'decisiones' | 'todo';
|
|
21
|
+
|
|
22
|
+
export interface SandboxOptions {
|
|
23
|
+
/** Entrada inicial, además de la que se dé con `entrada`. */
|
|
24
|
+
input?: string[];
|
|
25
|
+
/** Si `leer()` sin entrada pausa la ejecución para pedirla. Por defecto `true`. */
|
|
26
|
+
interactiveInput?: boolean;
|
|
27
|
+
/** Semilla de `aleatorio`. Si no se da, se elige una al cargar y se conserva en los retrocesos. */
|
|
28
|
+
seed?: number;
|
|
29
|
+
maxOperations?: number;
|
|
30
|
+
maxCallDepth?: number;
|
|
31
|
+
maxOutput?: number;
|
|
32
|
+
breakpoints?: number[];
|
|
33
|
+
/** Mundo del modo mundo, en el formato de `WorldDefinition`. */
|
|
34
|
+
world?: WorldDefinition;
|
|
35
|
+
/**
|
|
36
|
+
* `pasos`: solo `paso` y `robot`; `decisiones`: además `decide`; `todo`: todos los eventos; `nada`: ninguno.
|
|
37
|
+
* Por defecto `decisiones`.
|
|
38
|
+
*/
|
|
39
|
+
detail?: EventDetail;
|
|
40
|
+
/** Operaciones por tanda antes de ceder el control. Por defecto 50 000. */
|
|
41
|
+
batchSize?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type SandboxRequest =
|
|
45
|
+
| { type: 'cargar'; id: number; source: string; options?: SandboxOptions }
|
|
46
|
+
/** Ejecuta hasta terminar, sin detenerse en los puntos de interrupción. */
|
|
47
|
+
| { type: 'ejecutar'; id: number }
|
|
48
|
+
/** Ejecuta hasta el siguiente punto de interrupción, o hasta terminar. */
|
|
49
|
+
| { type: 'continuar'; id: number }
|
|
50
|
+
| { type: 'paso'; id: number }
|
|
51
|
+
| { type: 'pausar'; id: number }
|
|
52
|
+
/** Vuelve a ejecutar desde el inicio, con la misma entrada y semilla, hasta el paso `step`. */
|
|
53
|
+
| { type: 'ir_a_paso'; id: number; step: number }
|
|
54
|
+
| { type: 'entrada'; id: number; line: string }
|
|
55
|
+
| { type: 'cerrar_entrada'; id: number }
|
|
56
|
+
| { type: 'puntos_de_interrupción'; id: number; lines: number[] }
|
|
57
|
+
| { type: 'evaluar_reto'; id: number; source: string; challenge: Challenge };
|
|
58
|
+
|
|
59
|
+
export type StopReason = 'paso' | 'punto_de_interrupción' | 'pausa' | 'entrada' | 'fin' | 'error' | 'retroceso';
|
|
60
|
+
|
|
61
|
+
export interface Loaded {
|
|
62
|
+
type: 'cargado';
|
|
63
|
+
id: number;
|
|
64
|
+
tokens: Token[];
|
|
65
|
+
program: ast.Program;
|
|
66
|
+
/** Errores léxicos y sintácticos. Si hay alguno, no se puede ejecutar. */
|
|
67
|
+
errors: ErrorInfo[];
|
|
68
|
+
/** Semilla de `aleatorio` de esta carga. */
|
|
69
|
+
seed: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface Progress {
|
|
73
|
+
type: 'progreso';
|
|
74
|
+
id: number;
|
|
75
|
+
operations: number;
|
|
76
|
+
steps: number;
|
|
77
|
+
/** Salida nueva desde el mensaje anterior. */
|
|
78
|
+
output: string;
|
|
79
|
+
events: MachineEvent[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface Stopped {
|
|
83
|
+
type: 'detenido';
|
|
84
|
+
id: number;
|
|
85
|
+
status: MachineStatus;
|
|
86
|
+
reason: StopReason;
|
|
87
|
+
operations: number;
|
|
88
|
+
steps: number;
|
|
89
|
+
/** Salida nueva desde el mensaje anterior; con `replaceOutput`, toda la salida. */
|
|
90
|
+
output: string;
|
|
91
|
+
/** En un retroceso, la salida reemplaza a la anterior en lugar de agregarse. */
|
|
92
|
+
replaceOutput?: boolean;
|
|
93
|
+
events: MachineEvent[];
|
|
94
|
+
snapshot: Snapshot;
|
|
95
|
+
result?: SerializedValue;
|
|
96
|
+
error?: ErrorInfo;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type SandboxResponse =
|
|
100
|
+
| Loaded
|
|
101
|
+
| Progress
|
|
102
|
+
| Stopped
|
|
103
|
+
| { type: 'hecho'; id: number }
|
|
104
|
+
| { type: 'reto'; id: number; result: ChallengeResult }
|
|
105
|
+
/** El mensaje no se pudo atender (no hay programa cargado, la ejecución sigue en curso…). */
|
|
106
|
+
| { type: 'error'; id: number; message: string };
|
|
107
|
+
|
|
108
|
+
const DETAIL: Record<EventDetail, ReadonlySet<MachineEvent['type']> | 'todo'> = {
|
|
109
|
+
nada: new Set(),
|
|
110
|
+
pasos: new Set(['paso', 'robot']),
|
|
111
|
+
decisiones: new Set(['paso', 'robot', 'decide']),
|
|
112
|
+
todo: 'todo',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const tick = () => new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
116
|
+
|
|
117
|
+
export class SandboxHost {
|
|
118
|
+
private program?: ast.Program;
|
|
119
|
+
private options: SandboxOptions = {};
|
|
120
|
+
private seed = 0;
|
|
121
|
+
private machine?: Machine;
|
|
122
|
+
/** Líneas que se le dieron a `leer()` en esta carga, para repetirlas al retroceder. */
|
|
123
|
+
private inputLog: string[] = [];
|
|
124
|
+
private inputClosed = false;
|
|
125
|
+
private events: MachineEvent[] = [];
|
|
126
|
+
private muted = false;
|
|
127
|
+
private sentOutput = 0;
|
|
128
|
+
private running = false;
|
|
129
|
+
private pauseRequested = false;
|
|
130
|
+
|
|
131
|
+
constructor(private readonly post: (message: SandboxResponse) => void) {}
|
|
132
|
+
|
|
133
|
+
async handle(request: SandboxRequest): Promise<void> {
|
|
134
|
+
switch (request.type) {
|
|
135
|
+
case 'cargar':
|
|
136
|
+
if (this.running) return this.busy(request.id);
|
|
137
|
+
return this.load(request.id, request.source, request.options ?? {});
|
|
138
|
+
case 'evaluar_reto':
|
|
139
|
+
this.post({ type: 'reto', id: request.id, result: evaluarReto(request.source, request.challenge) });
|
|
140
|
+
return;
|
|
141
|
+
case 'pausar':
|
|
142
|
+
this.pauseRequested = this.running;
|
|
143
|
+
this.post({ type: 'hecho', id: request.id });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const machine = this.machine;
|
|
148
|
+
if (!machine) {
|
|
149
|
+
this.post({ type: 'error', id: request.id, message: 'No hay un programa cargado sin errores.' });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
switch (request.type) {
|
|
153
|
+
case 'entrada':
|
|
154
|
+
this.inputLog.push(request.line);
|
|
155
|
+
machine.provideInput(request.line);
|
|
156
|
+
this.post({ type: 'hecho', id: request.id });
|
|
157
|
+
return;
|
|
158
|
+
case 'cerrar_entrada':
|
|
159
|
+
this.inputClosed = true;
|
|
160
|
+
machine.closeInput();
|
|
161
|
+
this.post({ type: 'hecho', id: request.id });
|
|
162
|
+
return;
|
|
163
|
+
case 'puntos_de_interrupción':
|
|
164
|
+
this.options = { ...this.options, breakpoints: request.lines };
|
|
165
|
+
machine.setBreakpoints(request.lines);
|
|
166
|
+
this.post({ type: 'hecho', id: request.id });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (this.running) return this.busy(request.id);
|
|
171
|
+
switch (request.type) {
|
|
172
|
+
case 'paso':
|
|
173
|
+
machine.step();
|
|
174
|
+
this.stopped(request.id, machine);
|
|
175
|
+
return;
|
|
176
|
+
case 'ejecutar':
|
|
177
|
+
case 'continuar':
|
|
178
|
+
return this.runInBatches(request.id, machine, request.type === 'ejecutar');
|
|
179
|
+
case 'ir_a_paso':
|
|
180
|
+
this.goToStep(request.id, request.step);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private busy(id: number): void {
|
|
186
|
+
this.post({ type: 'error', id, message: 'La ejecución sigue en curso; pausa antes de mandar otra orden.' });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private load(id: number, source: string, options: SandboxOptions): void {
|
|
190
|
+
this.options = options;
|
|
191
|
+
this.seed = (options.seed ?? Math.floor(Math.random() * 2 ** 32)) >>> 0;
|
|
192
|
+
this.inputLog = [];
|
|
193
|
+
this.inputClosed = false;
|
|
194
|
+
this.events = [];
|
|
195
|
+
this.sentOutput = 0;
|
|
196
|
+
const { program, tokens, errors } = parse(source);
|
|
197
|
+
this.program = errors.length === 0 ? program : undefined;
|
|
198
|
+
this.machine = this.program ? this.createMachine() : undefined;
|
|
199
|
+
this.post({ type: 'cargado', id, tokens, program, errors: errors.map(error => error.toJSON()), seed: this.seed });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private createMachine(): Machine {
|
|
203
|
+
const { options } = this;
|
|
204
|
+
const levels = DETAIL[options.detail ?? 'decisiones'];
|
|
205
|
+
const machineOptions: MachineOptions = {
|
|
206
|
+
input: options.input ?? [],
|
|
207
|
+
interactiveInput: options.interactiveInput ?? true,
|
|
208
|
+
seed: this.seed,
|
|
209
|
+
breakpoints: options.breakpoints ?? [],
|
|
210
|
+
onEvent: event => {
|
|
211
|
+
if (this.muted) return;
|
|
212
|
+
if (levels === 'todo' || levels.has(event.type)) this.events.push(event);
|
|
213
|
+
},
|
|
214
|
+
...(options.maxOperations !== undefined ? { maxOperations: options.maxOperations } : {}),
|
|
215
|
+
...(options.maxCallDepth !== undefined ? { maxCallDepth: options.maxCallDepth } : {}),
|
|
216
|
+
...(options.maxOutput !== undefined ? { maxOutput: options.maxOutput } : {}),
|
|
217
|
+
...(options.world ? { world: createWorld(options.world) } : {}),
|
|
218
|
+
};
|
|
219
|
+
return new Machine(compile(this.program!), machineOptions);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private async runInBatches(id: number, machine: Machine, ignoreBreakpoints: boolean): Promise<void> {
|
|
223
|
+
this.running = true;
|
|
224
|
+
this.pauseRequested = false;
|
|
225
|
+
const breakpoints = this.options.breakpoints ?? [];
|
|
226
|
+
if (ignoreBreakpoints) machine.setBreakpoints([]);
|
|
227
|
+
try {
|
|
228
|
+
while (true) {
|
|
229
|
+
const status = machine.run(this.options.batchSize ?? 50_000);
|
|
230
|
+
if (status !== 'pausada' || machine.pauseReason !== 'presupuesto') break;
|
|
231
|
+
if (this.pauseRequested) break;
|
|
232
|
+
this.post({
|
|
233
|
+
type: 'progreso',
|
|
234
|
+
id,
|
|
235
|
+
operations: machine.operations,
|
|
236
|
+
steps: machine.steps,
|
|
237
|
+
output: this.newOutput(machine),
|
|
238
|
+
events: this.takeEvents(),
|
|
239
|
+
});
|
|
240
|
+
// Ceder el control deja que llegue un `pausar`.
|
|
241
|
+
await tick();
|
|
242
|
+
if (this.pauseRequested) break;
|
|
243
|
+
}
|
|
244
|
+
} finally {
|
|
245
|
+
if (ignoreBreakpoints) machine.setBreakpoints(breakpoints);
|
|
246
|
+
this.running = false;
|
|
247
|
+
}
|
|
248
|
+
this.stopped(id, machine, this.pauseRequested && machine.status === 'pausada' ? 'pausa' : undefined);
|
|
249
|
+
this.pauseRequested = false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private goToStep(id: number, step: number): void {
|
|
253
|
+
// Como la ejecución es determinista, otra máquina con la misma entrada y semilla llega al mismo estado.
|
|
254
|
+
const machine = this.createMachine();
|
|
255
|
+
for (const line of this.inputLog) machine.provideInput(line);
|
|
256
|
+
if (this.inputClosed) machine.closeInput();
|
|
257
|
+
this.muted = true;
|
|
258
|
+
try {
|
|
259
|
+
while (machine.steps < step && !machine.done && machine.status !== 'esperando_entrada') machine.step();
|
|
260
|
+
} finally {
|
|
261
|
+
this.muted = false;
|
|
262
|
+
}
|
|
263
|
+
this.machine = machine;
|
|
264
|
+
this.events = [];
|
|
265
|
+
this.sentOutput = 0;
|
|
266
|
+
this.stopped(id, machine, 'retroceso', true);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private newOutput(machine: Machine): string {
|
|
270
|
+
const output = machine.output;
|
|
271
|
+
const fresh = output.slice(this.sentOutput);
|
|
272
|
+
this.sentOutput = output.length;
|
|
273
|
+
return fresh;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private takeEvents(): MachineEvent[] {
|
|
277
|
+
const events = this.events;
|
|
278
|
+
this.events = [];
|
|
279
|
+
return events;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private stopped(id: number, machine: Machine, reason?: StopReason, replaceOutput = false): void {
|
|
283
|
+
const status = machine.status;
|
|
284
|
+
const finalReason: StopReason =
|
|
285
|
+
reason ??
|
|
286
|
+
(status === 'terminada'
|
|
287
|
+
? 'fin'
|
|
288
|
+
: status === 'error'
|
|
289
|
+
? 'error'
|
|
290
|
+
: status === 'esperando_entrada'
|
|
291
|
+
? 'entrada'
|
|
292
|
+
: machine.pauseReason === 'punto_de_interrupción'
|
|
293
|
+
? 'punto_de_interrupción'
|
|
294
|
+
: 'paso');
|
|
295
|
+
this.post({
|
|
296
|
+
type: 'detenido',
|
|
297
|
+
id,
|
|
298
|
+
status,
|
|
299
|
+
reason: finalReason,
|
|
300
|
+
operations: machine.operations,
|
|
301
|
+
steps: machine.steps,
|
|
302
|
+
output: this.newOutput(machine),
|
|
303
|
+
...(replaceOutput ? { replaceOutput: true } : {}),
|
|
304
|
+
events: this.takeEvents(),
|
|
305
|
+
snapshot: machine.snapshot(),
|
|
306
|
+
...(status === 'terminada' ? { result: serialize(machine.result) } : {}),
|
|
307
|
+
...(machine.error ? { error: machine.error.toJSON() } : {}),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
package/src/sandbox.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Challenge, ChallengeResult } from './challenge';
|
|
2
|
+
import type { Loaded, Progress, SandboxOptions, SandboxRequest, SandboxResponse, Stopped } from './sandbox-host';
|
|
3
|
+
|
|
4
|
+
/** Lo que `createSandbox` necesita de un worker (un `Worker` del navegador o de Bun). */
|
|
5
|
+
export interface WorkerLike {
|
|
6
|
+
postMessage(message: SandboxRequest): void;
|
|
7
|
+
addEventListener(type: 'message', listener: (event: MessageEvent<SandboxResponse>) => void): void;
|
|
8
|
+
terminate?(): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Sandbox {
|
|
12
|
+
/** Analiza el código; si no tiene errores, queda listo para ejecutarse. */
|
|
13
|
+
load(source: string, options?: SandboxOptions): Promise<Loaded>;
|
|
14
|
+
/** Ejecuta hasta terminar, sin detenerse en los puntos de interrupción. */
|
|
15
|
+
run(): Promise<Stopped>;
|
|
16
|
+
/** Ejecuta hasta el siguiente punto de interrupción, o hasta terminar. */
|
|
17
|
+
resume(): Promise<Stopped>;
|
|
18
|
+
step(): Promise<Stopped>;
|
|
19
|
+
/** Pide pausar; la promesa de `run()` o `resume()` se resuelve con el estado en el que quedó. */
|
|
20
|
+
pause(): Promise<void>;
|
|
21
|
+
/** Retrocede (o avanza) hasta el paso `n` volviendo a ejecutar desde el inicio. */
|
|
22
|
+
goToStep(step: number): Promise<Stopped>;
|
|
23
|
+
provideInput(line: string): Promise<void>;
|
|
24
|
+
closeInput(): Promise<void>;
|
|
25
|
+
setBreakpoints(lines: number[]): Promise<void>;
|
|
26
|
+
evaluarReto(source: string, challenge: Challenge): Promise<ChallengeResult>;
|
|
27
|
+
/** Progreso de cada tanda (operaciones, salida nueva, eventos). Regresa cómo dejar de escuchar. */
|
|
28
|
+
onProgress(listener: (progress: Progress) => void): () => void;
|
|
29
|
+
/** Cada vez que la ejecución se detiene. Regresa cómo dejar de escuchar. */
|
|
30
|
+
onStopped(listener: (stopped: Stopped) => void): () => void;
|
|
31
|
+
terminate(): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type Body<T extends SandboxRequest['type']> = Omit<Extract<SandboxRequest, { type: T }>, 'id'>;
|
|
35
|
+
|
|
36
|
+
/** Envuelve el worker de `codexivo/worker` con promesas y suscripciones. */
|
|
37
|
+
export const createSandbox = (worker: WorkerLike): Sandbox => {
|
|
38
|
+
let nextId = 1;
|
|
39
|
+
const pending = new Map<number, { resolve: (value: never) => void; reject: (error: Error) => void }>();
|
|
40
|
+
const progressListeners = new Set<(progress: Progress) => void>();
|
|
41
|
+
const stoppedListeners = new Set<(stopped: Stopped) => void>();
|
|
42
|
+
|
|
43
|
+
worker.addEventListener('message', ({ data }) => {
|
|
44
|
+
if (data.type === 'progreso') {
|
|
45
|
+
for (const listener of progressListeners) listener(data);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (data.type === 'detenido') for (const listener of stoppedListeners) listener(data);
|
|
49
|
+
const request = pending.get(data.id);
|
|
50
|
+
if (!request) return;
|
|
51
|
+
pending.delete(data.id);
|
|
52
|
+
if (data.type === 'error') request.reject(new Error(data.message));
|
|
53
|
+
else if (data.type === 'hecho') request.resolve(undefined as never);
|
|
54
|
+
else if (data.type === 'reto') request.resolve(data.result as never);
|
|
55
|
+
else request.resolve(data as never);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const send = <T extends SandboxRequest['type'], R>(type: T, body: Omit<Body<T>, 'type'> = {} as never) =>
|
|
59
|
+
new Promise<R>((resolve, reject) => {
|
|
60
|
+
const id = nextId++;
|
|
61
|
+
pending.set(id, { resolve: resolve as (value: never) => void, reject });
|
|
62
|
+
worker.postMessage({ type, id, ...body } as SandboxRequest);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const subscribe = <T>(listeners: Set<T>, listener: T) => {
|
|
66
|
+
listeners.add(listener);
|
|
67
|
+
return () => listeners.delete(listener);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
load: (source, options) => send<'cargar', Loaded>('cargar', { source, ...(options ? { options } : {}) }),
|
|
72
|
+
run: () => send<'ejecutar', Stopped>('ejecutar'),
|
|
73
|
+
resume: () => send<'continuar', Stopped>('continuar'),
|
|
74
|
+
step: () => send<'paso', Stopped>('paso'),
|
|
75
|
+
pause: () => send<'pausar', void>('pausar'),
|
|
76
|
+
goToStep: step => send<'ir_a_paso', Stopped>('ir_a_paso', { step }),
|
|
77
|
+
provideInput: line => send<'entrada', void>('entrada', { line }),
|
|
78
|
+
closeInput: () => send<'cerrar_entrada', void>('cerrar_entrada'),
|
|
79
|
+
setBreakpoints: lines => send<'puntos_de_interrupción', void>('puntos_de_interrupción', { lines }),
|
|
80
|
+
evaluarReto: (source, challenge) => send<'evaluar_reto', ChallengeResult>('evaluar_reto', { source, challenge }),
|
|
81
|
+
onProgress: listener => {
|
|
82
|
+
const unsubscribe = subscribe(progressListeners, listener);
|
|
83
|
+
return () => void unsubscribe();
|
|
84
|
+
},
|
|
85
|
+
onStopped: listener => {
|
|
86
|
+
const unsubscribe = subscribe(stoppedListeners, listener);
|
|
87
|
+
return () => void unsubscribe();
|
|
88
|
+
},
|
|
89
|
+
terminate: () => {
|
|
90
|
+
worker.terminate?.();
|
|
91
|
+
for (const request of pending.values()) request.reject(new Error('El sandbox se cerró.'));
|
|
92
|
+
pending.clear();
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
};
|
package/src/syntax/ast.ts
CHANGED
|
@@ -9,10 +9,12 @@ interface BaseNode {
|
|
|
9
9
|
span: Span;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/** `%` y `mod` son el mismo operador; en el AST siempre aparece como `mod`. */
|
|
13
|
+
export type BinaryOperator = '+' | '-' | '*' | '/' | 'div' | 'mod' | '^' | '==' | '!=' | '<' | '>' | '<=' | '>=';
|
|
13
14
|
export type LogicalOperator = 'y' | 'o';
|
|
14
15
|
export type UnaryOperator = '-' | 'no';
|
|
15
|
-
export type AssignmentOperator = '=' | '+=' | '-=' | '*=' | '/=';
|
|
16
|
+
export type AssignmentOperator = '=' | '+=' | '-=' | '*=' | '/=' | '%=';
|
|
17
|
+
export type IncrementOperator = '++' | '--';
|
|
16
18
|
|
|
17
19
|
// ── expresiones ──────────────────────────────────────────────────────────────
|
|
18
20
|
|
|
@@ -76,6 +78,20 @@ export interface Index extends BaseNode {
|
|
|
76
78
|
kind: 'Index';
|
|
77
79
|
object: Expression;
|
|
78
80
|
index: Expression;
|
|
81
|
+
/** `true` si se escribió `d.campo`; entonces `index` es el texto `"campo"`. */
|
|
82
|
+
dot?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface DictEntry {
|
|
86
|
+
/** Rango de `clave: valor`. */
|
|
87
|
+
span: Span;
|
|
88
|
+
key: Expression;
|
|
89
|
+
value: Expression;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface DictLiteral extends BaseNode {
|
|
93
|
+
kind: 'DictLiteral';
|
|
94
|
+
entries: DictEntry[];
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
export interface FunctionLiteral extends BaseNode {
|
|
@@ -92,6 +108,7 @@ export type Expression =
|
|
|
92
108
|
| BooleanLiteral
|
|
93
109
|
| NullLiteral
|
|
94
110
|
| ListLiteral
|
|
111
|
+
| DictLiteral
|
|
95
112
|
| Identifier
|
|
96
113
|
| Unary
|
|
97
114
|
| Binary
|
|
@@ -104,17 +121,36 @@ export type Expression =
|
|
|
104
121
|
|
|
105
122
|
export interface VariableDeclaration extends BaseNode {
|
|
106
123
|
kind: 'VariableDeclaration';
|
|
124
|
+
/** `true` si se declaró con `constante`: no se puede reasignar. */
|
|
125
|
+
constant: boolean;
|
|
107
126
|
name: Identifier;
|
|
108
127
|
value: Expression;
|
|
109
128
|
}
|
|
110
129
|
|
|
130
|
+
/** Lo que puede ir a la izquierda de `=`: una variable o un elemento (`lista[i]`). */
|
|
131
|
+
export type AssignmentTarget = Identifier | Index;
|
|
132
|
+
|
|
111
133
|
export interface Assignment extends BaseNode {
|
|
112
134
|
kind: 'Assignment';
|
|
113
|
-
target:
|
|
135
|
+
target: AssignmentTarget;
|
|
114
136
|
operator: AssignmentOperator;
|
|
115
137
|
value: Expression;
|
|
116
138
|
}
|
|
117
139
|
|
|
140
|
+
/** `i++` / `i--`: son sentencias, no expresiones. */
|
|
141
|
+
export interface Increment extends BaseNode {
|
|
142
|
+
kind: 'Increment';
|
|
143
|
+
target: AssignmentTarget;
|
|
144
|
+
operator: IncrementOperator;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** `procedimiento nombre(…) { … }`: se puede usar desde el inicio de su bloque. */
|
|
148
|
+
export interface FunctionDeclaration extends BaseNode {
|
|
149
|
+
kind: 'FunctionDeclaration';
|
|
150
|
+
name: Identifier;
|
|
151
|
+
function: FunctionLiteral;
|
|
152
|
+
}
|
|
153
|
+
|
|
118
154
|
export interface ExpressionStatement extends BaseNode {
|
|
119
155
|
kind: 'ExpressionStatement';
|
|
120
156
|
expression: Expression;
|
|
@@ -159,14 +195,78 @@ export interface DoUntil extends BaseNode {
|
|
|
159
195
|
|
|
160
196
|
export interface For extends BaseNode {
|
|
161
197
|
kind: 'For';
|
|
162
|
-
init?: VariableDeclaration | Assignment | ExpressionStatement;
|
|
198
|
+
init?: VariableDeclaration | Assignment | Increment | ExpressionStatement;
|
|
163
199
|
condition?: Expression;
|
|
164
|
-
update?: Assignment | ExpressionStatement;
|
|
200
|
+
update?: Assignment | Increment | ExpressionStatement;
|
|
201
|
+
body: Block;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* `para i desde a hasta b con_paso p { … }`: cuenta de `a` a `b`, incluidos los dos. Los límites y el paso se
|
|
206
|
+
* evalúan una vez, al empezar; `i` es nueva en cada vuelta y no se puede cambiar dentro del cuerpo.
|
|
207
|
+
*/
|
|
208
|
+
export interface ForRange extends BaseNode {
|
|
209
|
+
kind: 'ForRange';
|
|
210
|
+
variable: Identifier;
|
|
211
|
+
from: Expression;
|
|
212
|
+
to: Expression;
|
|
213
|
+
step?: Expression;
|
|
165
214
|
body: Block;
|
|
166
215
|
}
|
|
167
216
|
|
|
217
|
+
/** `repetir n veces { … }`: el contador es interno, no una variable visible. */
|
|
218
|
+
export interface Repeat extends BaseNode {
|
|
219
|
+
kind: 'Repeat';
|
|
220
|
+
count: Expression;
|
|
221
|
+
body: Block;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** `para_cada x en lista { … }`: recorre una lista o un texto. */
|
|
225
|
+
export interface ForEach extends BaseNode {
|
|
226
|
+
kind: 'ForEach';
|
|
227
|
+
variable: Identifier;
|
|
228
|
+
/** Segunda variable de `para_cada clave, valor en diccionario`. */
|
|
229
|
+
value?: Identifier;
|
|
230
|
+
iterable: Expression;
|
|
231
|
+
body: Block;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* `dominio nombre { … }`: un espacio de nombres. Agrupa declaraciones (variables, constantes, procedimientos y
|
|
236
|
+
* otros dominios) que se usan desde fuera con `nombre.miembro`. No es un módulo: no hay `importar`.
|
|
237
|
+
*/
|
|
238
|
+
export interface Domain extends BaseNode {
|
|
239
|
+
kind: 'Domain';
|
|
240
|
+
name: Identifier;
|
|
241
|
+
body: Block;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface Break extends BaseNode {
|
|
245
|
+
kind: 'Break';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface Continue extends BaseNode {
|
|
249
|
+
kind: 'Continue';
|
|
250
|
+
}
|
|
251
|
+
|
|
168
252
|
export type Statement =
|
|
169
|
-
|
|
253
|
+
| VariableDeclaration
|
|
254
|
+
| FunctionDeclaration
|
|
255
|
+
| Assignment
|
|
256
|
+
| Increment
|
|
257
|
+
| ExpressionStatement
|
|
258
|
+
| Return
|
|
259
|
+
| Block
|
|
260
|
+
| If
|
|
261
|
+
| While
|
|
262
|
+
| DoUntil
|
|
263
|
+
| For
|
|
264
|
+
| ForRange
|
|
265
|
+
| ForEach
|
|
266
|
+
| Repeat
|
|
267
|
+
| Break
|
|
268
|
+
| Continue
|
|
269
|
+
| Domain;
|
|
170
270
|
|
|
171
271
|
export interface Program extends BaseNode {
|
|
172
272
|
kind: 'Program';
|
|
@@ -183,6 +283,8 @@ export const childrenOf = (node: Node): Node[] => {
|
|
|
183
283
|
return node.statements;
|
|
184
284
|
case 'ListLiteral':
|
|
185
285
|
return node.elements;
|
|
286
|
+
case 'DictLiteral':
|
|
287
|
+
return node.entries.flatMap(entry => [entry.key, entry.value]);
|
|
186
288
|
case 'Unary':
|
|
187
289
|
return [node.operand];
|
|
188
290
|
case 'Binary':
|
|
@@ -198,6 +300,12 @@ export const childrenOf = (node: Node): Node[] => {
|
|
|
198
300
|
return [node.name, node.value];
|
|
199
301
|
case 'Assignment':
|
|
200
302
|
return [node.target, node.value];
|
|
303
|
+
case 'Increment':
|
|
304
|
+
return [node.target];
|
|
305
|
+
case 'FunctionDeclaration':
|
|
306
|
+
return [node.name, node.function];
|
|
307
|
+
case 'ForEach':
|
|
308
|
+
return [node.variable, ...(node.value ? [node.value] : []), node.iterable, node.body];
|
|
201
309
|
case 'ExpressionStatement':
|
|
202
310
|
return [node.expression];
|
|
203
311
|
case 'Return':
|
|
@@ -211,6 +319,12 @@ export const childrenOf = (node: Node): Node[] => {
|
|
|
211
319
|
return [node.condition, node.body];
|
|
212
320
|
case 'DoUntil':
|
|
213
321
|
return [node.body, node.condition];
|
|
322
|
+
case 'Domain':
|
|
323
|
+
return [node.name, node.body];
|
|
324
|
+
case 'ForRange':
|
|
325
|
+
return [node.variable, node.from, node.to, ...(node.step ? [node.step] : []), node.body];
|
|
326
|
+
case 'Repeat':
|
|
327
|
+
return [node.count, node.body];
|
|
214
328
|
case 'For':
|
|
215
329
|
return [
|
|
216
330
|
...(node.init ? [node.init] : []),
|
package/src/syntax/lexer.ts
CHANGED
|
@@ -20,6 +20,9 @@ const TWO_CHAR_OPERATORS: Readonly<Record<string, TokenType>> = {
|
|
|
20
20
|
'-=': TokenType.MINUS_ASSIGN,
|
|
21
21
|
'*=': TokenType.STAR_ASSIGN,
|
|
22
22
|
'/=': TokenType.SLASH_ASSIGN,
|
|
23
|
+
'%=': TokenType.PERCENT_ASSIGN,
|
|
24
|
+
'++': TokenType.INCREMENT,
|
|
25
|
+
'--': TokenType.DECREMENT,
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
const ONE_CHAR_OPERATORS: Readonly<Record<string, TokenType>> = {
|
|
@@ -28,10 +31,14 @@ const ONE_CHAR_OPERATORS: Readonly<Record<string, TokenType>> = {
|
|
|
28
31
|
'-': TokenType.MINUS,
|
|
29
32
|
'*': TokenType.STAR,
|
|
30
33
|
'/': TokenType.SLASH,
|
|
34
|
+
'%': TokenType.PERCENT,
|
|
35
|
+
'^': TokenType.CARET,
|
|
31
36
|
'!': TokenType.BANG,
|
|
32
37
|
'<': TokenType.LT,
|
|
33
38
|
'>': TokenType.GT,
|
|
34
39
|
',': TokenType.COMMA,
|
|
40
|
+
':': TokenType.COLON,
|
|
41
|
+
'.': TokenType.DOT,
|
|
35
42
|
';': TokenType.SEMICOLON,
|
|
36
43
|
'(': TokenType.LPAREN,
|
|
37
44
|
')': TokenType.RPAREN,
|
|
@@ -41,6 +48,15 @@ const ONE_CHAR_OPERATORS: Readonly<Record<string, TokenType>> = {
|
|
|
41
48
|
']': TokenType.RBRACKET,
|
|
42
49
|
};
|
|
43
50
|
|
|
51
|
+
const VALUE_ENDS = new Set([
|
|
52
|
+
TokenType.IDENT,
|
|
53
|
+
TokenType.NUMBER,
|
|
54
|
+
TokenType.STRING,
|
|
55
|
+
TokenType.RPAREN,
|
|
56
|
+
TokenType.RBRACKET,
|
|
57
|
+
TokenType.RBRACE,
|
|
58
|
+
]);
|
|
59
|
+
|
|
44
60
|
const ESCAPES: Readonly<Record<string, string>> = {
|
|
45
61
|
n: '\n',
|
|
46
62
|
t: '\t',
|
|
@@ -129,13 +145,20 @@ export const tokenize = (source: string): LexResult => {
|
|
|
129
145
|
}
|
|
130
146
|
|
|
131
147
|
// números
|
|
132
|
-
|
|
133
|
-
|
|
148
|
+
// Después de un valor, `.` es acceso a un campo (`d.1` es un error claro, no `d` seguido de `.1`).
|
|
149
|
+
const last = tokens[tokens.length - 1];
|
|
150
|
+
const afterValue = last !== undefined && VALUE_ENDS.has(last.type) && last.span.end.offset === offset;
|
|
151
|
+
if (DIGIT.test(character) || (character === '.' && DIGIT.test(peek(1)) && !afterValue)) {
|
|
152
|
+
// `_` separa grupos de dígitos (`1_000_000`); se revisa que esté bien puesto al final.
|
|
153
|
+
const digits = () => {
|
|
154
|
+
while (DIGIT.test(peek()) || peek() === '_') advance();
|
|
155
|
+
};
|
|
156
|
+
digits();
|
|
134
157
|
if (peek() === '.' && DIGIT.test(peek(1))) {
|
|
135
158
|
advance();
|
|
136
|
-
|
|
159
|
+
digits();
|
|
137
160
|
}
|
|
138
|
-
if (LETTER.test(peek())) {
|
|
161
|
+
if (LETTER.test(peek()) && peek() !== '_') {
|
|
139
162
|
while (LETTER.test(peek()) || DIGIT.test(peek())) advance();
|
|
140
163
|
const token = push(TokenType.ILLEGAL, start);
|
|
141
164
|
lexicalError(
|
|
@@ -146,7 +169,18 @@ export const tokenize = (source: string): LexResult => {
|
|
|
146
169
|
);
|
|
147
170
|
continue;
|
|
148
171
|
}
|
|
149
|
-
|
|
172
|
+
const text = source.slice(start.offset, offset);
|
|
173
|
+
if (/(^|\D)_|_(\D|$)/.test(text)) {
|
|
174
|
+
const token = push(TokenType.ILLEGAL, start);
|
|
175
|
+
lexicalError(
|
|
176
|
+
'SEPARADOR_INVÁLIDO',
|
|
177
|
+
`\`${text}\` no es un número válido.`,
|
|
178
|
+
token.span,
|
|
179
|
+
'El `_` solo puede ir entre dos dígitos para separar grupos, como en `1_000_000`.',
|
|
180
|
+
);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
push(TokenType.NUMBER, start, Number(text.replaceAll('_', '')));
|
|
150
184
|
continue;
|
|
151
185
|
}
|
|
152
186
|
|