codexivo 0.2.0 → 1.0.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/src/flow.ts ADDED
@@ -0,0 +1,385 @@
1
+ import type * as ast from './syntax/ast';
2
+ import type { Span } from './syntax/token';
3
+ import { printExpression, printStatement } from './printer';
4
+
5
+ /**
6
+ * Diagrama de flujo generado desde el AST. Aquí se decide qué nodos y aristas existen; el sitio solo los
7
+ * acomoda (con ELK o dagre) y los dibuja. Con los eventos `paso` y `decide` de la máquina (`nodeId`), el sitio
8
+ * sabe qué nodo iluminar: cada `nodeId` que emite la ejecución aparece en `astIds` de algún nodo del grafo.
9
+ */
10
+
11
+ export type FlowNodeKind = 'inicio' | 'fin' | 'proceso' | 'decisión' | 'llamada' | 'entrada' | 'salida' | 'retorno';
12
+
13
+ export interface FlowNode {
14
+ id: string;
15
+ kind: FlowNodeKind;
16
+ /** Texto corto para mostrar, generado desde el AST. Si se agruparon sentencias, una por línea. */
17
+ label: string;
18
+ /** Nodos del AST que representa: la sentencia y, en una decisión, también su condición. */
19
+ astIds: number[];
20
+ spans: Span[];
21
+ /** Orden sugerido para acomodar el grafo: distancia desde el inicio sin contar las vueltas de los bucles. */
22
+ rank: number;
23
+ /** En una llamada a un procedimiento del programa: su nombre, para enlazar con `procedures`. */
24
+ procedure?: string;
25
+ }
26
+
27
+ /**
28
+ * - `normal`: sigue a la siguiente sentencia (o entra o sale de un bucle);
29
+ * - `vuelta`: regresa al inicio de un bucle;
30
+ * - `romper`, `continuar`, `regresa`: saltos explícitos.
31
+ */
32
+ export type FlowEdgeKind = 'normal' | 'vuelta' | 'romper' | 'continuar' | 'regresa';
33
+
34
+ /** Etiqueta de una arista que sale de una decisión. */
35
+ export type FlowEdgeLabel = 'sí' | 'no' | 'repetir' | 'salir';
36
+
37
+ export interface FlowEdge {
38
+ id: string;
39
+ from: string;
40
+ to: string;
41
+ kind: FlowEdgeKind;
42
+ label?: FlowEdgeLabel;
43
+ }
44
+
45
+ export interface FlowGraph {
46
+ nodes: FlowNode[];
47
+ edges: FlowEdge[];
48
+ /** Con `expandProcedures`, el grafo de cada procedimiento con nombre del programa. */
49
+ procedures?: Array<{ name: string; astId: number; graph: FlowGraph }>;
50
+ }
51
+
52
+ export interface FlowOptions {
53
+ /** Junta sentencias simples consecutivas (asignaciones, declaraciones) en un solo nodo de proceso. */
54
+ group?: boolean;
55
+ /** Genera también el grafo de cada procedimiento con nombre y marca las llamadas a ellos. */
56
+ expandProcedures?: boolean;
57
+ /** Recorta las etiquetas largas a esta cantidad de caracteres, con `…`. Por defecto no se recortan. */
58
+ maxLabelLength?: number;
59
+ }
60
+
61
+ interface Pending {
62
+ from: string;
63
+ kind?: FlowEdgeKind;
64
+ label?: FlowEdgeLabel;
65
+ }
66
+
67
+ interface Loop {
68
+ breaks: Pending[];
69
+ continues: Pending[];
70
+ }
71
+
72
+ const INPUT = new Set(['leer', 'leer_número', 'leer_numero']);
73
+ const OUTPUT = new Set(['imprimir']);
74
+
75
+ /** `f` o `dominio.f`: la ruta de un nombre, si la expresión es solo eso. */
76
+ const path = (node: ast.Expression): string | undefined => {
77
+ if (node.kind === 'Identifier') return node.name;
78
+ if (node.kind === 'Index' && node.dot && node.index.kind === 'StringLiteral') {
79
+ const object = path(node.object);
80
+ return object && `${object}.${node.index.value}`;
81
+ }
82
+ return undefined;
83
+ };
84
+
85
+ /** Nombre (o ruta) del procedimiento al que llama una expresión, si es una llamada directa. */
86
+ const calledName = (node: ast.Expression | undefined): string | undefined =>
87
+ node?.kind === 'Call' ? path(node.callee) : undefined;
88
+
89
+ class Builder {
90
+ readonly nodes: FlowNode[] = [];
91
+ readonly edges: FlowEdge[] = [];
92
+ private readonly loops: Loop[] = [];
93
+ private readonly returns: Pending[] = [];
94
+ /** Último nodo de proceso, al que se le pueden juntar sentencias con `group`. */
95
+ private groupable?: string;
96
+
97
+ constructor(
98
+ private readonly options: FlowOptions,
99
+ private readonly procedures: Set<string>,
100
+ ) {}
101
+
102
+ private label(text: string): string {
103
+ const max = this.options.maxLabelLength;
104
+ return max && text.length > max ? `${text.slice(0, Math.max(1, max - 1))}…` : text;
105
+ }
106
+
107
+ node(kind: FlowNodeKind, label: string, astNodes: Array<{ id: number; span: Span }>): string {
108
+ const id = `n${this.nodes.length}`;
109
+ this.nodes.push({
110
+ id,
111
+ kind,
112
+ label: this.label(label),
113
+ astIds: astNodes.map(node => node.id),
114
+ spans: astNodes.map(node => node.span),
115
+ rank: 0,
116
+ });
117
+ this.groupable = undefined;
118
+ return id;
119
+ }
120
+
121
+ connect(pending: Pending[], to: string): void {
122
+ for (const { from, kind = 'normal', label } of pending) {
123
+ this.edges.push({ id: `e${this.edges.length}`, from, to, kind, ...(label ? { label } : {}) });
124
+ }
125
+ }
126
+
127
+ /** Un nodo simple con sus aristas de entrada; regresa la arista pendiente que sale de él. */
128
+ private simple(kind: FlowNodeKind, statement: ast.Statement, incoming: Pending[], procedure?: string): Pending[] {
129
+ const groupable = kind === 'proceso' && this.options.group;
130
+ const last = this.groupable ? this.nodes.find(node => node.id === this.groupable) : undefined;
131
+ if (groupable && last && incoming.length === 1 && incoming[0]!.from === last.id && !incoming[0]!.label) {
132
+ last.label = `${last.label}\n${this.label(printStatement(statement))}`;
133
+ last.astIds.push(statement.id);
134
+ last.spans.push(statement.span);
135
+ return incoming;
136
+ }
137
+ const id = this.node(kind, printStatement(statement), [statement]);
138
+ if (procedure) this.nodes[this.nodes.length - 1]!.procedure = procedure;
139
+ this.connect(incoming, id);
140
+ if (groupable) this.groupable = id;
141
+ return [{ from: id }];
142
+ }
143
+
144
+ private kindOf(statement: ast.Statement): { kind: FlowNodeKind; procedure?: string } {
145
+ const expression =
146
+ statement.kind === 'ExpressionStatement'
147
+ ? statement.expression
148
+ : statement.kind === 'VariableDeclaration' || statement.kind === 'Assignment'
149
+ ? statement.value
150
+ : undefined;
151
+ const name = calledName(expression);
152
+ if (name && INPUT.has(name)) return { kind: 'entrada' };
153
+ if (name && OUTPUT.has(name) && statement.kind === 'ExpressionStatement') return { kind: 'salida' };
154
+ if (name && this.procedures.has(name)) {
155
+ return this.options.expandProcedures ? { kind: 'llamada', procedure: name } : { kind: 'llamada' };
156
+ }
157
+ if (statement.kind === 'ExpressionStatement' && name) return { kind: 'llamada' };
158
+ return { kind: 'proceso' };
159
+ }
160
+
161
+ statements(list: ast.Statement[], incoming: Pending[]): Pending[] {
162
+ let current = incoming;
163
+ for (const statement of list) current = this.statement(statement, current);
164
+ return current;
165
+ }
166
+
167
+ private loop(body: () => Pending[]): { out: Pending[]; loop: Loop } {
168
+ const loop: Loop = { breaks: [], continues: [] };
169
+ this.loops.push(loop);
170
+ const out = body();
171
+ this.loops.pop();
172
+ return { out, loop };
173
+ }
174
+
175
+ private back(pending: Pending[], kind: FlowEdgeKind = 'vuelta'): Pending[] {
176
+ return pending.map(edge => (edge.kind && edge.kind !== 'normal' ? edge : { ...edge, kind }));
177
+ }
178
+
179
+ private statement(statement: ast.Statement, incoming: Pending[]): Pending[] {
180
+ switch (statement.kind) {
181
+ case 'FunctionDeclaration':
182
+ // Se crea al entrar al bloque, no en este punto; su grafo va en `procedures`.
183
+ return incoming;
184
+ case 'Domain': {
185
+ // Un dominio solo declara: es un paso de proceso que representa todas sus declaraciones.
186
+ const members: ast.Statement[] = [];
187
+ const collect = (domain: ast.Domain) => {
188
+ members.push(domain);
189
+ for (const member of domain.body.statements) {
190
+ if (member.kind === 'VariableDeclaration') members.push(member);
191
+ else if (member.kind === 'Domain') collect(member);
192
+ }
193
+ };
194
+ collect(statement);
195
+ const id = this.node('proceso', printStatement(statement), members);
196
+ this.connect(incoming, id);
197
+ return [{ from: id }];
198
+ }
199
+ case 'VariableDeclaration':
200
+ case 'Assignment':
201
+ case 'Increment':
202
+ case 'ExpressionStatement': {
203
+ const { kind, procedure } = this.kindOf(statement);
204
+ return this.simple(kind, statement, incoming, procedure);
205
+ }
206
+ case 'Return': {
207
+ const id = this.node('retorno', printStatement(statement), [statement]);
208
+ this.connect(incoming, id);
209
+ this.returns.push({ from: id, kind: 'regresa' });
210
+ return [];
211
+ }
212
+ case 'Break':
213
+ case 'Continue': {
214
+ const id = this.node('proceso', printStatement(statement), [statement]);
215
+ this.connect(incoming, id);
216
+ const loop = this.loops[this.loops.length - 1];
217
+ if (statement.kind === 'Break') loop?.breaks.push({ from: id, kind: 'romper' });
218
+ else loop?.continues.push({ from: id, kind: 'continuar' });
219
+ return [];
220
+ }
221
+ case 'Block':
222
+ return this.statements(statement.statements, incoming);
223
+ case 'If': {
224
+ let pending = incoming;
225
+ const out: Pending[] = [];
226
+ statement.branches.forEach((branch, index) => {
227
+ const astNodes = index === 0 ? [statement, branch.condition] : [branch.condition];
228
+ const id = this.node('decisión', printExpression(branch.condition), astNodes);
229
+ this.connect(pending, id);
230
+ out.push(...this.statements(branch.body.statements, [{ from: id, label: 'sí' }]));
231
+ pending = [{ from: id, label: 'no' }];
232
+ });
233
+ if (statement.alternative) out.push(...this.statements(statement.alternative.statements, pending));
234
+ else out.push(...pending);
235
+ return out;
236
+ }
237
+ case 'While': {
238
+ const id = this.node('decisión', printExpression(statement.condition), [statement, statement.condition]);
239
+ this.connect(incoming, id);
240
+ const { out, loop } = this.loop(() => this.statements(statement.body.statements, [{ from: id, label: 'sí' }]));
241
+ this.connect(this.back([...out, ...loop.continues]), id);
242
+ return [{ from: id, label: 'no' }, ...loop.breaks];
243
+ }
244
+ case 'DoUntil': {
245
+ // El cuerpo va primero; la entrada al bucle se conecta al primer nodo que cree.
246
+ const first = this.nodes.length;
247
+ const { out, loop } = this.loop(() => this.statements(statement.body.statements, incoming));
248
+ const id = this.node('decisión', `hasta_que ${printExpression(statement.condition)}`, [
249
+ statement,
250
+ statement.condition,
251
+ ]);
252
+ this.connect([...out, ...loop.continues], id);
253
+ const start = this.nodes[first]?.id ?? id;
254
+ this.connect([{ from: id, label: 'no', kind: 'vuelta' }], start);
255
+ if (start === id) this.edges.pop(); // cuerpo vacío: la vuelta sería un lazo sobre sí misma
256
+ return [{ from: id, label: 'sí' }, ...loop.breaks];
257
+ }
258
+ case 'For': {
259
+ let pending = incoming;
260
+ const initOwner: ast.Statement[] = [];
261
+ if (statement.init) {
262
+ this.groupable = undefined;
263
+ const before = this.nodes.length;
264
+ pending = this.statement(statement.init, pending);
265
+ // El paso del `para` se ilumina en su inicio.
266
+ this.nodes[before]?.astIds.unshift(statement.id);
267
+ this.nodes[before]?.spans.unshift(statement.span);
268
+ } else {
269
+ initOwner.push(statement);
270
+ }
271
+ const condition = statement.condition;
272
+ const id = this.node('decisión', condition ? printExpression(condition) : 'verdadero', [
273
+ ...initOwner,
274
+ ...(condition ? [condition] : []),
275
+ ]);
276
+ this.connect(pending, id);
277
+ const { out, loop } = this.loop(() => this.statements(statement.body.statements, [{ from: id, label: 'sí' }]));
278
+ let tail = [...out, ...loop.continues];
279
+ if (statement.update) tail = this.statement(statement.update, tail);
280
+ this.connect(this.back(tail), id);
281
+ return [...(condition ? [{ from: id, label: 'no' as const }] : []), ...loop.breaks];
282
+ }
283
+ case 'Repeat':
284
+ case 'ForRange':
285
+ case 'ForEach': {
286
+ const head =
287
+ statement.kind === 'Repeat'
288
+ ? statement.count
289
+ : statement.kind === 'ForRange'
290
+ ? statement.to
291
+ : statement.iterable;
292
+ const id = this.node('decisión', printStatement(statement), [statement, head]);
293
+ this.connect(incoming, id);
294
+ const { out, loop } = this.loop(() =>
295
+ this.statements(statement.body.statements, [{ from: id, label: 'repetir' }]),
296
+ );
297
+ this.connect(this.back([...out, ...loop.continues]), id);
298
+ return [{ from: id, label: 'salir' }, ...loop.breaks];
299
+ }
300
+ }
301
+ }
302
+
303
+ finish(start: string, out: Pending[]): void {
304
+ const end = this.node('fin', 'fin', []);
305
+ this.connect([...out, ...this.returns], end);
306
+ // Rango: distancia más larga desde el inicio sin contar vueltas (el grafo sin ellas no tiene ciclos).
307
+ const forward = this.edges.filter(edge => edge.kind !== 'vuelta' && edge.kind !== 'continuar');
308
+ const rank = new Map<string, number>([[start, 0]]);
309
+ for (let changed = true, guard = 0; changed && guard <= this.nodes.length; guard++) {
310
+ changed = false;
311
+ for (const edge of forward) {
312
+ const next = (rank.get(edge.from) ?? -1) + 1;
313
+ if (rank.has(edge.from) && next > (rank.get(edge.to) ?? -1)) {
314
+ rank.set(edge.to, next);
315
+ changed = true;
316
+ }
317
+ }
318
+ }
319
+ for (const node of this.nodes) node.rank = rank.get(node.id) ?? 0;
320
+ }
321
+ }
322
+
323
+ /** Procedimientos con nombre declarados en estas sentencias, con su ruta si están en un dominio. */
324
+ const procedureNames = (statements: ast.Statement[], known: Iterable<string> = []): Set<string> => {
325
+ const names = new Set(known);
326
+ const visit = (list: ast.Statement[], prefix: string) => {
327
+ for (const statement of list) {
328
+ if (statement.kind === 'FunctionDeclaration') names.add(`${prefix}${statement.name.name}`);
329
+ if (statement.kind === 'Domain') visit(statement.body.statements, `${prefix}${statement.name.name}.`);
330
+ }
331
+ };
332
+ visit(statements, '');
333
+ return names;
334
+ };
335
+
336
+ /**
337
+ * Grafo de flujo de un programa o de un procedimiento. El AST tiene que venir sin errores de sintaxis.
338
+ */
339
+ export const flowGraph = (
340
+ root: ast.Program | ast.FunctionLiteral | ast.FunctionDeclaration,
341
+ options: FlowOptions = {},
342
+ ): FlowGraph => graphOf(root, options, []);
343
+
344
+ const graphOf = (
345
+ root: ast.Program | ast.FunctionLiteral | ast.FunctionDeclaration,
346
+ options: FlowOptions,
347
+ known: Iterable<string>,
348
+ ): FlowGraph => {
349
+ const statements =
350
+ root.kind === 'Program'
351
+ ? root.statements
352
+ : root.kind === 'FunctionDeclaration'
353
+ ? root.function.body.statements
354
+ : root.body.statements;
355
+ const names = procedureNames(statements, known);
356
+ const builder = new Builder(options, names);
357
+ const title =
358
+ root.kind === 'Program'
359
+ ? 'inicio'
360
+ : root.kind === 'FunctionDeclaration'
361
+ ? printStatement(root)
362
+ : `procedimiento(${root.params.map(param => param.name).join(', ')})`;
363
+ const start = builder.node('inicio', title, root.kind === 'Program' ? [] : [root]);
364
+ const out = builder.statements(statements, [{ from: start }]);
365
+ builder.finish(start, out);
366
+
367
+ const graph: FlowGraph = { nodes: builder.nodes, edges: builder.edges };
368
+ if (options.expandProcedures) {
369
+ graph.procedures = [];
370
+ // Los procedimientos de un dominio se nombran con su ruta: `calculadora.suma`.
371
+ const visit = (list: ast.Statement[], prefix: string) => {
372
+ for (const statement of list) {
373
+ if (statement.kind === 'Domain') visit(statement.body.statements, `${prefix}${statement.name.name}.`);
374
+ if (statement.kind !== 'FunctionDeclaration') continue;
375
+ graph.procedures!.push({
376
+ name: `${prefix}${statement.name.name}`,
377
+ astId: statement.id,
378
+ graph: graphOf(statement, options, names),
379
+ });
380
+ }
381
+ };
382
+ visit(statements, '');
383
+ }
384
+ return graph;
385
+ };
package/src/index.ts CHANGED
@@ -23,7 +23,59 @@ export type {
23
23
  SerializedValue,
24
24
  Snapshot,
25
25
  FrameSnapshot,
26
+ HeapEntry,
26
27
  } from './runtime/machine';
27
28
  export { natives } from './runtime/builtins';
28
29
  export { repr, show, typeOf, equals, Scope, WAIT_FOR_INPUT } from './runtime/values';
29
- export type { Value, ListValue, ProcedureValue, NativeValue, NativeContext, TypeName } from './runtime/values';
30
+ export type {
31
+ Value,
32
+ ListValue,
33
+ DictValue,
34
+ DictKey,
35
+ ProcedureValue,
36
+ NativeValue,
37
+ NativeContext,
38
+ TypeName,
39
+ } from './runtime/values';
40
+
41
+ export { createWorld, validateWorld, checkGoal, DIRECTIONS } from './world';
42
+ export type {
43
+ World,
44
+ WorldDefinition,
45
+ WorldState,
46
+ WorldGoal,
47
+ WorldValidationError,
48
+ GoalDifference,
49
+ Wall,
50
+ BeeperPile,
51
+ BeeperCount,
52
+ Robot,
53
+ RobotEvent,
54
+ RobotSnapshot,
55
+ RobotAction,
56
+ Direction,
57
+ } from './world';
58
+
59
+ export { catalog } from './catalog';
60
+ export type { Catalog, KeywordGroup, OperatorEntry, ProcedureEntry, ConstantEntry, ErrorEntry } from './catalog';
61
+ export type { ProcedureDoc, ProcedureExample, ProcedureLevel, ProcedureCategory } from './runtime/values';
62
+
63
+ export { flowGraph } from './flow';
64
+ export type { FlowGraph, FlowNode, FlowEdge, FlowNodeKind, FlowEdgeKind, FlowEdgeLabel, FlowOptions } from './flow';
65
+ export { printExpression, printStatement } from './printer';
66
+
67
+ export { evaluarReto } from './challenge';
68
+ export type { Challenge, ChallengeCase, ChallengeResult, CaseResult, JsonValue } from './challenge';
69
+ export { createSandbox } from './sandbox';
70
+ export type { Sandbox, WorkerLike } from './sandbox';
71
+ export { SandboxHost } from './sandbox-host';
72
+ export type {
73
+ SandboxOptions,
74
+ SandboxRequest,
75
+ SandboxResponse,
76
+ EventDetail,
77
+ Loaded,
78
+ Progress,
79
+ Stopped,
80
+ StopReason,
81
+ } from './sandbox-host';
package/src/printer.ts ADDED
@@ -0,0 +1,133 @@
1
+ import type * as ast from './syntax/ast';
2
+ import { repr } from './runtime/values';
3
+
4
+ /**
5
+ * Convierte nodos del AST en texto corto de Codexivo (`suma += i`, `i <= 10`). Lo usan el diagrama de flujo y
6
+ * cualquier vista que necesite mostrar una parte del programa sin copiar el código original.
7
+ */
8
+
9
+ /** Precedencia de la §4.1 (mayor agrupa primero), para poner paréntesis solo donde hacen falta. */
10
+ const precedence = (node: ast.Expression): number => {
11
+ switch (node.kind) {
12
+ case 'Logical':
13
+ return node.operator === 'o' ? 1 : 2;
14
+ case 'Unary':
15
+ return node.operator === 'no' ? 3 : 7;
16
+ case 'Binary':
17
+ switch (node.operator) {
18
+ case '==':
19
+ case '!=':
20
+ case '<':
21
+ case '>':
22
+ case '<=':
23
+ case '>=':
24
+ return 4;
25
+ case '+':
26
+ case '-':
27
+ return 5;
28
+ case '^':
29
+ return 8;
30
+ default:
31
+ return 6;
32
+ }
33
+ default:
34
+ return 10;
35
+ }
36
+ };
37
+
38
+ const operand = (node: ast.Expression, parent: number, wrapEqual: boolean): string => {
39
+ const text = printExpression(node);
40
+ const own = precedence(node);
41
+ return own < parent || (wrapEqual && own === parent) ? `(${text})` : text;
42
+ };
43
+
44
+ export const printExpression = (node: ast.Expression): string => {
45
+ switch (node.kind) {
46
+ case 'NumberLiteral':
47
+ case 'StringLiteral':
48
+ case 'BooleanLiteral':
49
+ return repr(node.value);
50
+ case 'NullLiteral':
51
+ return 'nulo';
52
+ case 'Identifier':
53
+ return node.name;
54
+ case 'ListLiteral':
55
+ return `[${node.elements.map(printExpression).join(', ')}]`;
56
+ case 'DictLiteral':
57
+ return node.entries.length === 0
58
+ ? '{}'
59
+ : `{ ${node.entries
60
+ .map(entry => {
61
+ const key = printExpression(entry.key);
62
+ const literal = entry.key.kind === 'StringLiteral' || entry.key.kind === 'NumberLiteral';
63
+ return `${literal ? key : `(${key})`}: ${printExpression(entry.value)}`;
64
+ })
65
+ .join(', ')} }`;
66
+ case 'Unary': {
67
+ const own = precedence(node);
68
+ const inner = operand(node.operand, own, false);
69
+ return node.operator === 'no' ? `no ${inner}` : `-${inner}`;
70
+ }
71
+ case 'Binary':
72
+ case 'Logical': {
73
+ const own = precedence(node);
74
+ // `^` agrupa a la derecha; los demás, a la izquierda. Las comparaciones no se encadenan.
75
+ const right = node.kind === 'Binary' && node.operator === '^';
76
+ const compare = own === 4;
77
+ const left = operand(node.left, own, right || compare);
78
+ const rightText = operand(node.right, own, !right || compare);
79
+ return `${left} ${node.operator} ${rightText}`;
80
+ }
81
+ case 'Call':
82
+ return `${operand(node.callee, 10, false)}(${node.args.map(printExpression).join(', ')})`;
83
+ case 'Index':
84
+ if (node.dot && node.index.kind === 'StringLiteral')
85
+ return `${operand(node.object, 10, false)}.${node.index.value}`;
86
+ return `${operand(node.object, 10, false)}[${printExpression(node.index)}]`;
87
+ case 'FunctionLiteral':
88
+ return `procedimiento(${node.params.map(param => param.name).join(', ')}) { … }`;
89
+ }
90
+ };
91
+
92
+ /**
93
+ * Una sentencia en una línea. Las compuestas se resumen con su encabezado: `si (x > 0)`, `repetir 3 veces`,
94
+ * `para_cada x en lista`.
95
+ */
96
+ export const printStatement = (node: ast.Statement): string => {
97
+ switch (node.kind) {
98
+ case 'VariableDeclaration':
99
+ return `${node.constant ? 'constante' : 'variable'} ${node.name.name} = ${printExpression(node.value)}`;
100
+ case 'FunctionDeclaration':
101
+ return `procedimiento ${node.name.name}(${node.function.params.map(param => param.name).join(', ')})`;
102
+ case 'Assignment':
103
+ return `${printExpression(node.target)} ${node.operator} ${printExpression(node.value)}`;
104
+ case 'Increment':
105
+ return `${printExpression(node.target)}${node.operator}`;
106
+ case 'ExpressionStatement':
107
+ return printExpression(node.expression);
108
+ case 'Return':
109
+ return node.value ? `regresa ${printExpression(node.value)}` : 'regresa';
110
+ case 'Domain':
111
+ return `dominio ${node.name.name}`;
112
+ case 'Break':
113
+ return 'romper';
114
+ case 'Continue':
115
+ return 'continuar';
116
+ case 'Block':
117
+ return '{ … }';
118
+ case 'If':
119
+ return `si (${printExpression(node.branches[0]!.condition)})`;
120
+ case 'While':
121
+ return `mientras (${printExpression(node.condition)})`;
122
+ case 'DoUntil':
123
+ return `hasta_que (${printExpression(node.condition)})`;
124
+ case 'For':
125
+ return `para (${node.condition ? printExpression(node.condition) : ''})`;
126
+ case 'Repeat':
127
+ return `repetir ${printExpression(node.count)} veces`;
128
+ case 'ForRange':
129
+ return `para ${node.variable.name} desde ${printExpression(node.from)} hasta ${printExpression(node.to)}${node.step ? ` con_paso ${printExpression(node.step)}` : ''}`;
130
+ case 'ForEach':
131
+ return `para_cada ${node.variable.name}${node.value ? `, ${node.value.name}` : ''} en ${printExpression(node.iterable)}`;
132
+ }
133
+ };