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/src/errors.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { Span } from './syntax/token';
2
+
3
+ export type ErrorKind = 'léxico' | 'sintaxis' | 'ejecución' | 'límite';
4
+
5
+ export interface ErrorInfo {
6
+ kind: ErrorKind;
7
+ code: string;
8
+ message: string;
9
+ hint?: string;
10
+ span?: Span;
11
+ /** Nombres de los procedimientos activos cuando ocurrió el error, del más externo al más interno. */
12
+ callStack?: string[];
13
+ }
14
+
15
+ /**
16
+ * Error de Codexivo. Siempre describe qué pasó (`message`) y, cuando es posible,
17
+ * qué revisar (`hint`) y dónde (`span`).
18
+ */
19
+ export class CodexivoError extends Error implements ErrorInfo {
20
+ kind: ErrorKind;
21
+ code: string;
22
+ hint?: string;
23
+ span?: Span;
24
+ callStack?: string[];
25
+
26
+ constructor(info: ErrorInfo) {
27
+ super(info.message);
28
+ this.name = 'CodexivoError';
29
+ this.kind = info.kind;
30
+ this.code = info.code;
31
+ this.hint = info.hint;
32
+ this.span = info.span;
33
+ this.callStack = info.callStack;
34
+ }
35
+
36
+ toJSON(): ErrorInfo {
37
+ return {
38
+ kind: this.kind,
39
+ code: this.code,
40
+ message: this.message,
41
+ hint: this.hint,
42
+ span: this.span,
43
+ callStack: this.callStack,
44
+ };
45
+ }
46
+ }
47
+
48
+ /** Texto legible de un error, como lo muestran el REPL y la consola del sandbox. */
49
+ export const formatError = (error: ErrorInfo): string => {
50
+ const where = error.span ? ` en la línea ${error.span.start.line}, columna ${error.span.start.column}` : '';
51
+ const lines = [`Error de ${error.kind}${where}: ${error.message}`];
52
+ if (error.hint) {
53
+ lines.push(`Pista: ${error.hint}`);
54
+ }
55
+ return lines.join('\n');
56
+ };
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ export { prepare, run, Session } from './engine';
2
+ export type { PreparedProgram, RunOptions, RunResult } from './engine';
3
+
4
+ export { CodexivoError, formatError } from './errors';
5
+ export type { ErrorInfo, ErrorKind } from './errors';
6
+
7
+ export { tokenize } from './syntax/lexer';
8
+ export type { LexResult } from './syntax/lexer';
9
+ export { parse } from './syntax/parser';
10
+ export type { ParseResult } from './syntax/parser';
11
+ export { TokenType, KEYWORDS, isKeyword } from './syntax/token';
12
+ export type { Token, Span, Position } from './syntax/token';
13
+ export { childrenOf, walk } from './syntax/ast';
14
+ export type * as ast from './syntax/ast';
15
+
16
+ export { compile } from './runtime/compiler';
17
+ export type { Chunk, Instruction, DecisionLabel } from './runtime/compiler';
18
+ export { Machine, serialize } from './runtime/machine';
19
+ export type {
20
+ MachineEvent,
21
+ MachineOptions,
22
+ MachineStatus,
23
+ SerializedValue,
24
+ Snapshot,
25
+ FrameSnapshot,
26
+ } from './runtime/machine';
27
+ export { natives } from './runtime/builtins';
28
+ export { repr, show, typeOf, equals, Scope, WAIT_FOR_INPUT } from './runtime/values';
29
+ export type { Value, ListValue, ProcedureValue, NativeValue, NativeContext, TypeName } from './runtime/values';
@@ -0,0 +1,133 @@
1
+ import { describeType, isList, repr, show, typeOf, WAIT_FOR_INPUT } from './values';
2
+ import type { NativeContext, NativeValue, Value } from './values';
3
+
4
+ type NativeFn = (args: Value[], context: NativeContext) => Value | typeof WAIT_FOR_INPUT;
5
+
6
+ const define = (name: string, arity: NativeValue['arity'], call: NativeFn): NativeValue => ({
7
+ kind: 'nativo',
8
+ name,
9
+ arity,
10
+ call,
11
+ });
12
+
13
+ const expectNumber = (name: string, value: Value, context: NativeContext, position = ''): number => {
14
+ if (typeof value === 'number') return value;
15
+ throw context.fail(
16
+ 'TIPO_DE_ARGUMENTO',
17
+ `\`${name}\` espera un número${position}, pero recibió ${describeType(typeOf(value))} (${repr(value)}).`,
18
+ );
19
+ };
20
+
21
+ const expectList = (name: string, value: Value, context: NativeContext) => {
22
+ if (isList(value)) return value;
23
+ throw context.fail('TIPO_DE_ARGUMENTO', `\`${name}\` espera una lista, pero recibió ${describeType(typeOf(value))}.`);
24
+ };
25
+
26
+ const numbersOf = (name: string, args: Value[], context: NativeContext): number[] => {
27
+ const values = args.length === 1 && isList(args[0]!) ? args[0].items : args;
28
+ if (values.length === 0) {
29
+ throw context.fail('LISTA_VACÍA', `\`${name}\` necesita al menos un número, pero la lista está vacía.`);
30
+ }
31
+ return values.map(value => expectNumber(name, value, context));
32
+ };
33
+
34
+ const parseNumber = (text: string): number | undefined => {
35
+ const trimmed = text.trim();
36
+ if (!/^[-+]?(\d+(\.\d*)?|\.\d+)$/.test(trimmed)) return undefined;
37
+ return Number(trimmed);
38
+ };
39
+
40
+ /** Redondeo escolar: los .5 se alejan del cero (2.5 → 3, -2.5 → -3). */
41
+ const roundHalfAway = (value: number, decimals: number): number => {
42
+ const factor = 10 ** decimals;
43
+ return (Math.sign(value) * Math.round(Math.abs(value) * factor)) / factor;
44
+ };
45
+
46
+ const library: NativeValue[] = [
47
+ // entrada y salida
48
+ define('imprimir', [0, Infinity], (args, context) => {
49
+ context.write(`${args.map(show).join(' ')}\n`);
50
+ return null;
51
+ }),
52
+ define('leer', 0, (_args, context) => context.readLine()),
53
+ define('leer_número', 0, (_args, context) => {
54
+ const line = context.readLine();
55
+ if (line === WAIT_FOR_INPUT) return line;
56
+ if (line === null) throw context.fail('SIN_ENTRADA', '`leer_número` no encontró más datos de entrada.');
57
+ const value = parseNumber(line);
58
+ if (value === undefined) {
59
+ throw context.fail('NO_ES_NÚMERO', `\`leer_número\` esperaba un número, pero leyó ${JSON.stringify(line)}.`);
60
+ }
61
+ return value;
62
+ }),
63
+
64
+ // tipos y conversiones
65
+ define('tipo', 1, args => typeOf(args[0]!)),
66
+ define('texto', 1, args => show(args[0]!)),
67
+ define('número', 1, (args, context) => {
68
+ const value = args[0]!;
69
+ if (typeof value === 'number') return value;
70
+ if (typeof value === 'string') {
71
+ const parsed = parseNumber(value);
72
+ if (parsed !== undefined) return parsed;
73
+ throw context.fail('NO_ES_NÚMERO', `No se puede convertir ${JSON.stringify(value)} en número.`);
74
+ }
75
+ throw context.fail('TIPO_DE_ARGUMENTO', `No se puede convertir ${describeType(typeOf(value))} en número.`);
76
+ }),
77
+ define('longitud', 1, (args, context) => {
78
+ const value = args[0]!;
79
+ if (typeof value === 'string') return [...value].length;
80
+ if (isList(value)) return value.items.length;
81
+ throw context.fail(
82
+ 'TIPO_DE_ARGUMENTO',
83
+ `\`longitud\` espera un texto o una lista, pero recibió ${describeType(typeOf(value))}.`,
84
+ );
85
+ }),
86
+
87
+ // matemáticas
88
+ define('absoluto', 1, (args, context) => Math.abs(expectNumber('absoluto', args[0]!, context))),
89
+ define('máximo', [1, Infinity], (args, context) => Math.max(...numbersOf('máximo', args, context))),
90
+ define('mínimo', [1, Infinity], (args, context) => Math.min(...numbersOf('mínimo', args, context))),
91
+ define('redondear', [1, 2], (args, context) => {
92
+ const value = expectNumber('redondear', args[0]!, context);
93
+ const decimals = args.length > 1 ? expectNumber('redondear', args[1]!, context, ' de decimales') : 0;
94
+ if (!Number.isInteger(decimals) || decimals < 0) {
95
+ throw context.fail(
96
+ 'TIPO_DE_ARGUMENTO',
97
+ 'La cantidad de decimales de `redondear` tiene que ser un entero positivo o 0.',
98
+ );
99
+ }
100
+ return roundHalfAway(value, decimals);
101
+ }),
102
+
103
+ // listas
104
+ define('agregar', 2, (args, context) => {
105
+ expectList('agregar', args[0]!, context).items.push(args[1]!);
106
+ return null;
107
+ }),
108
+ define('primero', 1, (args, context) => {
109
+ const list = expectList('primero', args[0]!, context);
110
+ if (list.items.length === 0) throw context.fail('LISTA_VACÍA', '`primero` no puede tomar nada de una lista vacía.');
111
+ return list.items[0]!;
112
+ }),
113
+ define('último', 1, (args, context) => {
114
+ const list = expectList('último', args[0]!, context);
115
+ if (list.items.length === 0) throw context.fail('LISTA_VACÍA', '`último` no puede tomar nada de una lista vacía.');
116
+ return list.items[list.items.length - 1]!;
117
+ }),
118
+ ];
119
+
120
+ /** Quita los acentos agudos y la diéresis, pero conserva la ñ. */
121
+ export const withoutAccents = (name: string): string =>
122
+ name.replace(/[áéíóúü]/g, character => ({ á: 'a', é: 'e', í: 'i', ó: 'o', ú: 'u', ü: 'u' })[character] ?? character);
123
+
124
+ /** Biblioteca estándar. Cada nombre con acento también se acepta sin él (`máximo` y `maximo`). */
125
+ export const natives: Record<string, NativeValue> = (() => {
126
+ const table: Record<string, NativeValue> = {};
127
+ for (const native of library) table[native.name] = native;
128
+ for (const native of library) {
129
+ const alias = withoutAccents(native.name);
130
+ if (!table[alias]) table[alias] = native;
131
+ }
132
+ return table;
133
+ })();
@@ -0,0 +1,252 @@
1
+ import type * as ast from '../syntax/ast';
2
+ import type { Value } from './values';
3
+
4
+ export type DecisionLabel = 'si' | 'pero_si' | 'mientras' | 'hasta_que' | 'para';
5
+
6
+ /**
7
+ * Instrucciones de la máquina de Codexivo. Cada una conserva el nodo del AST que la originó, así la
8
+ * máquina puede decir qué parte del código está ejecutando y dónde ocurrió un error.
9
+ */
10
+ export type Instruction =
11
+ /** Inicio de una sentencia: punto natural para pausar. */
12
+ | { op: 'step'; node: ast.Statement }
13
+ | { op: 'const'; value: Value; node: ast.Expression }
14
+ | { op: 'list'; count: number; node: ast.ListLiteral }
15
+ | { op: 'load'; name: string; node: ast.Identifier; forUpdate?: boolean }
16
+ /** Saca un valor de la pila y crea la variable en el alcance actual. */
17
+ | { op: 'declare'; name: string; node: ast.VariableDeclaration }
18
+ /** Saca un valor de la pila y lo guarda en una variable existente. */
19
+ | { op: 'store'; name: string; node: ast.Assignment }
20
+ /** Saca un valor de la pila; es el valor de una sentencia de expresión. */
21
+ | { op: 'result'; node: ast.ExpressionStatement }
22
+ | { op: 'unary'; operator: ast.UnaryOperator; node: ast.Unary }
23
+ | { op: 'binary'; operator: ast.BinaryOperator; node: ast.Binary | ast.Assignment }
24
+ | { op: 'index'; node: ast.Index }
25
+ | { op: 'closure'; chunk: Chunk; node: ast.FunctionLiteral }
26
+ | { op: 'call'; argc: number; node: ast.Call }
27
+ | { op: 'return'; hasValue: boolean; node: ast.Return }
28
+ | { op: 'jump'; target: number }
29
+ /** Saca la condición (debe ser lógica) y salta si vale `jumpWhen`. */
30
+ | {
31
+ op: 'branch';
32
+ target: number;
33
+ jumpWhen: boolean;
34
+ label: DecisionLabel;
35
+ node: ast.Expression;
36
+ owner: ast.Statement;
37
+ }
38
+ /** Cortocircuito: revisa el lado izquierdo de `y`/`o`; si ya decide el resultado, lo deja y salta. */
39
+ | { op: 'shortCircuit'; target: number; node: ast.Logical }
40
+ /** Revisa que el lado derecho de `y`/`o` sea lógico. */
41
+ | { op: 'checkLogical'; node: ast.Logical }
42
+ | { op: 'enterScope' }
43
+ | { op: 'exitScope' }
44
+ | { op: 'halt' };
45
+
46
+ /** Código de un procedimiento o del programa principal. */
47
+ export interface Chunk {
48
+ name: string;
49
+ params: string[];
50
+ code: Instruction[];
51
+ node: ast.Program | ast.FunctionLiteral;
52
+ }
53
+
54
+ /** Traduce el AST a instrucciones. El AST debe venir sin errores de sintaxis. */
55
+ export const compile = (program: ast.Program): Chunk => {
56
+ const compiler = new Compiler({ name: 'programa', params: [], code: [], node: program });
57
+ for (const statement of program.statements) compiler.statement(statement);
58
+ compiler.emit({ op: 'halt' });
59
+ return compiler.chunk;
60
+ };
61
+
62
+ const compileFunction = (node: ast.FunctionLiteral): Chunk => {
63
+ const compiler = new Compiler({
64
+ name: node.name ?? 'procedimiento',
65
+ params: node.params.map(param => param.name),
66
+ code: [],
67
+ node,
68
+ });
69
+ // El cuerpo comparte alcance con los parámetros: así `variable x` dentro de `procedimiento(x)` es un error.
70
+ for (const statement of node.body.statements) compiler.statement(statement);
71
+ compiler.emit({ op: 'const', value: null, node });
72
+ compiler.emit({ op: 'return', hasValue: true, node: { kind: 'Return', id: node.id, span: node.body.span } });
73
+ return compiler.chunk;
74
+ };
75
+
76
+ class Compiler {
77
+ constructor(readonly chunk: Chunk) {}
78
+
79
+ emit(instruction: Instruction): number {
80
+ this.chunk.code.push(instruction);
81
+ return this.chunk.code.length - 1;
82
+ }
83
+
84
+ private get here(): number {
85
+ return this.chunk.code.length;
86
+ }
87
+
88
+ private patch(at: number, target: number): void {
89
+ const instruction = this.chunk.code[at];
90
+ if (instruction && 'target' in instruction) instruction.target = target;
91
+ }
92
+
93
+ statement(node: ast.Statement): void {
94
+ if (node.kind !== 'Block') this.emit({ op: 'step', node });
95
+ switch (node.kind) {
96
+ case 'VariableDeclaration':
97
+ this.expression(node.value);
98
+ this.emit({ op: 'declare', name: node.name.name, node });
99
+ return;
100
+ case 'Assignment':
101
+ if (node.operator !== '=') {
102
+ this.emit({ op: 'load', name: node.target.name, node: node.target, forUpdate: true });
103
+ }
104
+ this.expression(node.value);
105
+ if (node.operator !== '=') {
106
+ this.emit({ op: 'binary', operator: node.operator[0] as ast.BinaryOperator, node });
107
+ }
108
+ this.emit({ op: 'store', name: node.target.name, node });
109
+ return;
110
+ case 'ExpressionStatement':
111
+ this.expression(node.expression);
112
+ this.emit({ op: 'result', node });
113
+ return;
114
+ case 'Return':
115
+ if (node.value) this.expression(node.value);
116
+ this.emit({ op: 'return', hasValue: Boolean(node.value), node });
117
+ return;
118
+ case 'Block':
119
+ this.block(node);
120
+ return;
121
+ case 'If': {
122
+ const exits: number[] = [];
123
+ node.branches.forEach((branch, index) => {
124
+ this.expression(branch.condition);
125
+ const skip = this.emit({
126
+ op: 'branch',
127
+ target: -1,
128
+ jumpWhen: false,
129
+ label: index === 0 ? 'si' : 'pero_si',
130
+ node: branch.condition,
131
+ owner: node,
132
+ });
133
+ this.block(branch.body);
134
+ exits.push(this.emit({ op: 'jump', target: -1 }));
135
+ this.patch(skip, this.here);
136
+ });
137
+ if (node.alternative) this.block(node.alternative);
138
+ for (const exit of exits) this.patch(exit, this.here);
139
+ return;
140
+ }
141
+ case 'While': {
142
+ const start = this.here;
143
+ this.expression(node.condition);
144
+ const exit = this.emit({
145
+ op: 'branch',
146
+ target: -1,
147
+ jumpWhen: false,
148
+ label: 'mientras',
149
+ node: node.condition,
150
+ owner: node,
151
+ });
152
+ this.block(node.body);
153
+ this.emit({ op: 'jump', target: start });
154
+ this.patch(exit, this.here);
155
+ return;
156
+ }
157
+ case 'DoUntil': {
158
+ const start = this.here;
159
+ this.block(node.body);
160
+ this.expression(node.condition);
161
+ this.emit({
162
+ op: 'branch',
163
+ target: start,
164
+ jumpWhen: false,
165
+ label: 'hasta_que',
166
+ node: node.condition,
167
+ owner: node,
168
+ });
169
+ return;
170
+ }
171
+ case 'For': {
172
+ this.emit({ op: 'enterScope' });
173
+ if (node.init) this.statement(node.init);
174
+ const start = this.here;
175
+ let exit = -1;
176
+ if (node.condition) {
177
+ this.expression(node.condition);
178
+ exit = this.emit({
179
+ op: 'branch',
180
+ target: -1,
181
+ jumpWhen: false,
182
+ label: 'para',
183
+ node: node.condition,
184
+ owner: node,
185
+ });
186
+ }
187
+ this.block(node.body);
188
+ if (node.update) this.statement(node.update);
189
+ this.emit({ op: 'jump', target: start });
190
+ if (exit >= 0) this.patch(exit, this.here);
191
+ this.emit({ op: 'exitScope' });
192
+ return;
193
+ }
194
+ }
195
+ }
196
+
197
+ private block(node: ast.Block): void {
198
+ this.emit({ op: 'enterScope' });
199
+ for (const statement of node.statements) this.statement(statement);
200
+ this.emit({ op: 'exitScope' });
201
+ }
202
+
203
+ private expression(node: ast.Expression): void {
204
+ switch (node.kind) {
205
+ case 'NumberLiteral':
206
+ case 'StringLiteral':
207
+ case 'BooleanLiteral':
208
+ this.emit({ op: 'const', value: node.value, node });
209
+ return;
210
+ case 'NullLiteral':
211
+ this.emit({ op: 'const', value: null, node });
212
+ return;
213
+ case 'ListLiteral':
214
+ for (const element of node.elements) this.expression(element);
215
+ this.emit({ op: 'list', count: node.elements.length, node });
216
+ return;
217
+ case 'Identifier':
218
+ this.emit({ op: 'load', name: node.name, node });
219
+ return;
220
+ case 'Unary':
221
+ this.expression(node.operand);
222
+ this.emit({ op: 'unary', operator: node.operator, node });
223
+ return;
224
+ case 'Binary':
225
+ this.expression(node.left);
226
+ this.expression(node.right);
227
+ this.emit({ op: 'binary', operator: node.operator, node });
228
+ return;
229
+ case 'Logical': {
230
+ this.expression(node.left);
231
+ const skip = this.emit({ op: 'shortCircuit', target: -1, node });
232
+ this.expression(node.right);
233
+ this.emit({ op: 'checkLogical', node });
234
+ this.patch(skip, this.here);
235
+ return;
236
+ }
237
+ case 'Call':
238
+ this.expression(node.callee);
239
+ for (const arg of node.args) this.expression(arg);
240
+ this.emit({ op: 'call', argc: node.args.length, node });
241
+ return;
242
+ case 'Index':
243
+ this.expression(node.object);
244
+ this.expression(node.index);
245
+ this.emit({ op: 'index', node });
246
+ return;
247
+ case 'FunctionLiteral':
248
+ this.emit({ op: 'closure', chunk: compileFunction(node), node });
249
+ return;
250
+ }
251
+ }
252
+ }