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,33 @@
|
|
|
1
|
+
const distance = (a: string, b: string): number => {
|
|
2
|
+
const previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
3
|
+
for (let i = 1; i <= a.length; i++) {
|
|
4
|
+
let diagonal = previous[0]!;
|
|
5
|
+
previous[0] = i;
|
|
6
|
+
for (let j = 1; j <= b.length; j++) {
|
|
7
|
+
const above = previous[j]!;
|
|
8
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
9
|
+
previous[j] = Math.min(previous[j]! + 1, previous[j - 1]! + 1, diagonal + cost);
|
|
10
|
+
diagonal = above;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return previous[b.length]!;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const fold = (text: string): string => text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
|
17
|
+
|
|
18
|
+
/** El nombre más parecido a `name` entre `candidates`, si hay uno razonablemente cerca. */
|
|
19
|
+
export const suggestName = (name: string, candidates: Iterable<string>): string | undefined => {
|
|
20
|
+
const target = fold(name);
|
|
21
|
+
const limit = target.length <= 3 ? 1 : 2;
|
|
22
|
+
let best: string | undefined;
|
|
23
|
+
let bestDistance = Infinity;
|
|
24
|
+
for (const candidate of new Set(candidates)) {
|
|
25
|
+
if (candidate === name) continue;
|
|
26
|
+
const current = distance(target, fold(candidate));
|
|
27
|
+
if (current <= limit && current < bestDistance) {
|
|
28
|
+
best = candidate;
|
|
29
|
+
bestDistance = current;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return best;
|
|
33
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { CodexivoError } from '../errors';
|
|
2
|
+
import type { Chunk } from './compiler';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Valores en tiempo de ejecución. Números, textos, lógicos y `nulo` son primitivos de JavaScript;
|
|
6
|
+
* listas y procedimientos son objetos con un `id` para poder dibujar cuándo dos variables
|
|
7
|
+
* comparten el mismo valor.
|
|
8
|
+
*/
|
|
9
|
+
export type Value = number | string | boolean | null | ListValue | ProcedureValue | NativeValue;
|
|
10
|
+
|
|
11
|
+
export interface ListValue {
|
|
12
|
+
kind: 'lista';
|
|
13
|
+
id: number;
|
|
14
|
+
items: Value[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ProcedureValue {
|
|
18
|
+
kind: 'procedimiento';
|
|
19
|
+
id: number;
|
|
20
|
+
name?: string;
|
|
21
|
+
chunk: Chunk;
|
|
22
|
+
closure: Scope;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Contexto que la máquina le da a los procedimientos nativos. */
|
|
26
|
+
export interface NativeContext {
|
|
27
|
+
write(text: string): void;
|
|
28
|
+
/** Siguiente línea de entrada, `null` si ya no hay, o `WAIT_FOR_INPUT` si hay que pedirla. */
|
|
29
|
+
readLine(): string | null | typeof WAIT_FOR_INPUT;
|
|
30
|
+
newList(items: Value[]): ListValue;
|
|
31
|
+
/** Error de ejecución ubicado en la llamada actual. */
|
|
32
|
+
fail(code: string, message: string, hint?: string): CodexivoError;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const WAIT_FOR_INPUT: unique symbol = Symbol('esperar-entrada');
|
|
36
|
+
|
|
37
|
+
export interface NativeValue {
|
|
38
|
+
kind: 'nativo';
|
|
39
|
+
name: string;
|
|
40
|
+
/** Cantidad de argumentos: exacta, o `[mínimo, máximo]` (`máximo` puede ser `Infinity`). */
|
|
41
|
+
arity: number | [number, number];
|
|
42
|
+
call(args: Value[], context: NativeContext): Value | typeof WAIT_FOR_INPUT;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type TypeName = 'número' | 'texto' | 'lógico' | 'nulo' | 'lista' | 'procedimiento';
|
|
46
|
+
|
|
47
|
+
export const typeOf = (value: Value): TypeName => {
|
|
48
|
+
if (value === null) return 'nulo';
|
|
49
|
+
switch (typeof value) {
|
|
50
|
+
case 'number':
|
|
51
|
+
return 'número';
|
|
52
|
+
case 'string':
|
|
53
|
+
return 'texto';
|
|
54
|
+
case 'boolean':
|
|
55
|
+
return 'lógico';
|
|
56
|
+
default:
|
|
57
|
+
return value.kind === 'lista' ? 'lista' : 'procedimiento';
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** "un número", "una lista"… para mensajes. */
|
|
62
|
+
export const describeType = (type: TypeName): string => {
|
|
63
|
+
switch (type) {
|
|
64
|
+
case 'lista':
|
|
65
|
+
return 'una lista';
|
|
66
|
+
case 'nulo':
|
|
67
|
+
return '`nulo`';
|
|
68
|
+
default:
|
|
69
|
+
return `un ${type}`;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const isList = (value: Value): value is ListValue =>
|
|
74
|
+
typeof value === 'object' && value !== null && value.kind === 'lista';
|
|
75
|
+
|
|
76
|
+
export const isCallable = (value: Value): value is ProcedureValue | NativeValue =>
|
|
77
|
+
typeof value === 'object' && value !== null && (value.kind === 'procedimiento' || value.kind === 'nativo');
|
|
78
|
+
|
|
79
|
+
const formatNumber = (value: number): string => {
|
|
80
|
+
if (Object.is(value, -0)) return '0';
|
|
81
|
+
if (value === Infinity) return 'infinito';
|
|
82
|
+
if (value === -Infinity) return '-infinito';
|
|
83
|
+
return String(value);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Representación como se escribiría en código: los textos van entre comillas. */
|
|
87
|
+
export const repr = (value: Value, seen: Set<ListValue> = new Set()): string => {
|
|
88
|
+
if (value === null) return 'nulo';
|
|
89
|
+
switch (typeof value) {
|
|
90
|
+
case 'number':
|
|
91
|
+
return formatNumber(value);
|
|
92
|
+
case 'string':
|
|
93
|
+
return JSON.stringify(value);
|
|
94
|
+
case 'boolean':
|
|
95
|
+
return value ? 'verdadero' : 'falso';
|
|
96
|
+
}
|
|
97
|
+
if (value.kind === 'lista') {
|
|
98
|
+
if (seen.has(value)) return '[…]';
|
|
99
|
+
seen.add(value);
|
|
100
|
+
const inner = value.items.map(item => repr(item, seen)).join(', ');
|
|
101
|
+
seen.delete(value);
|
|
102
|
+
return `[${inner}]`;
|
|
103
|
+
}
|
|
104
|
+
return value.name ? `procedimiento ${value.name}` : 'procedimiento';
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Como lo muestra `imprimir`: los textos sin comillas. */
|
|
108
|
+
export const show = (value: Value): string => (typeof value === 'string' ? value : repr(value));
|
|
109
|
+
|
|
110
|
+
/** Igualdad por valor: dos listas son iguales si tienen los mismos elementos en el mismo orden. */
|
|
111
|
+
export const equals = (a: Value, b: Value, seen: Array<[ListValue, ListValue]> = []): boolean => {
|
|
112
|
+
if (a === b) return true;
|
|
113
|
+
if (isList(a) && isList(b)) {
|
|
114
|
+
if (a.items.length !== b.items.length) return false;
|
|
115
|
+
if (seen.some(([x, y]) => x === a && y === b)) return true;
|
|
116
|
+
seen.push([a, b]);
|
|
117
|
+
const result = a.items.every((item, index) => equals(item, b.items[index]!, seen));
|
|
118
|
+
seen.pop();
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Un alcance: las variables de un bloque. `parent` apunta al alcance que lo contiene.
|
|
126
|
+
*/
|
|
127
|
+
export class Scope {
|
|
128
|
+
readonly variables = new Map<string, Value>();
|
|
129
|
+
|
|
130
|
+
constructor(readonly parent?: Scope) {}
|
|
131
|
+
|
|
132
|
+
lookup(name: string): Scope | undefined {
|
|
133
|
+
let scope: Scope | undefined = this;
|
|
134
|
+
while (scope) {
|
|
135
|
+
if (scope.variables.has(name)) return scope;
|
|
136
|
+
scope = scope.parent;
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { Span } from './token';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* El AST está hecho de objetos planos: se puede serializar, enviar a un Web Worker y dibujar sin
|
|
5
|
+
* perder información. Cada nodo tiene un `id` único dentro de su programa y su rango en el código.
|
|
6
|
+
*/
|
|
7
|
+
interface BaseNode {
|
|
8
|
+
id: number;
|
|
9
|
+
span: Span;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type BinaryOperator = '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '>' | '<=' | '>=';
|
|
13
|
+
export type LogicalOperator = 'y' | 'o';
|
|
14
|
+
export type UnaryOperator = '-' | 'no';
|
|
15
|
+
export type AssignmentOperator = '=' | '+=' | '-=' | '*=' | '/=';
|
|
16
|
+
|
|
17
|
+
// ── expresiones ──────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export interface NumberLiteral extends BaseNode {
|
|
20
|
+
kind: 'NumberLiteral';
|
|
21
|
+
value: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface StringLiteral extends BaseNode {
|
|
25
|
+
kind: 'StringLiteral';
|
|
26
|
+
value: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BooleanLiteral extends BaseNode {
|
|
30
|
+
kind: 'BooleanLiteral';
|
|
31
|
+
value: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface NullLiteral extends BaseNode {
|
|
35
|
+
kind: 'NullLiteral';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ListLiteral extends BaseNode {
|
|
39
|
+
kind: 'ListLiteral';
|
|
40
|
+
elements: Expression[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Identifier extends BaseNode {
|
|
44
|
+
kind: 'Identifier';
|
|
45
|
+
name: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Unary extends BaseNode {
|
|
49
|
+
kind: 'Unary';
|
|
50
|
+
operator: UnaryOperator;
|
|
51
|
+
operand: Expression;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface Binary extends BaseNode {
|
|
55
|
+
kind: 'Binary';
|
|
56
|
+
operator: BinaryOperator;
|
|
57
|
+
left: Expression;
|
|
58
|
+
right: Expression;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `y` / `o`: se evalúan en cortocircuito, por eso no son un `Binary`. */
|
|
62
|
+
export interface Logical extends BaseNode {
|
|
63
|
+
kind: 'Logical';
|
|
64
|
+
operator: LogicalOperator;
|
|
65
|
+
left: Expression;
|
|
66
|
+
right: Expression;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface Call extends BaseNode {
|
|
70
|
+
kind: 'Call';
|
|
71
|
+
callee: Expression;
|
|
72
|
+
args: Expression[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface Index extends BaseNode {
|
|
76
|
+
kind: 'Index';
|
|
77
|
+
object: Expression;
|
|
78
|
+
index: Expression;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface FunctionLiteral extends BaseNode {
|
|
82
|
+
kind: 'FunctionLiteral';
|
|
83
|
+
/** Nombre con el que se declaró, si se conoce (`variable doble = procedimiento…` → `doble`). */
|
|
84
|
+
name?: string;
|
|
85
|
+
params: Identifier[];
|
|
86
|
+
body: Block;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type Expression =
|
|
90
|
+
| NumberLiteral
|
|
91
|
+
| StringLiteral
|
|
92
|
+
| BooleanLiteral
|
|
93
|
+
| NullLiteral
|
|
94
|
+
| ListLiteral
|
|
95
|
+
| Identifier
|
|
96
|
+
| Unary
|
|
97
|
+
| Binary
|
|
98
|
+
| Logical
|
|
99
|
+
| Call
|
|
100
|
+
| Index
|
|
101
|
+
| FunctionLiteral;
|
|
102
|
+
|
|
103
|
+
// ── sentencias ───────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
export interface VariableDeclaration extends BaseNode {
|
|
106
|
+
kind: 'VariableDeclaration';
|
|
107
|
+
name: Identifier;
|
|
108
|
+
value: Expression;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface Assignment extends BaseNode {
|
|
112
|
+
kind: 'Assignment';
|
|
113
|
+
target: Identifier;
|
|
114
|
+
operator: AssignmentOperator;
|
|
115
|
+
value: Expression;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ExpressionStatement extends BaseNode {
|
|
119
|
+
kind: 'ExpressionStatement';
|
|
120
|
+
expression: Expression;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface Return extends BaseNode {
|
|
124
|
+
kind: 'Return';
|
|
125
|
+
value?: Expression;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface Block extends BaseNode {
|
|
129
|
+
kind: 'Block';
|
|
130
|
+
statements: Statement[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface IfBranch {
|
|
134
|
+
/** Rango de la rama completa: desde `si` / `pero_si` hasta su `}`. */
|
|
135
|
+
span: Span;
|
|
136
|
+
condition: Expression;
|
|
137
|
+
body: Block;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** `si … pero_si … si_no`: las ramas se guardan en orden, no anidadas. */
|
|
141
|
+
export interface If extends BaseNode {
|
|
142
|
+
kind: 'If';
|
|
143
|
+
branches: IfBranch[];
|
|
144
|
+
alternative?: Block;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface While extends BaseNode {
|
|
148
|
+
kind: 'While';
|
|
149
|
+
condition: Expression;
|
|
150
|
+
body: Block;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** `hacer { … } hasta_que (condición)`: repite el cuerpo hasta que la condición sea verdadera. */
|
|
154
|
+
export interface DoUntil extends BaseNode {
|
|
155
|
+
kind: 'DoUntil';
|
|
156
|
+
body: Block;
|
|
157
|
+
condition: Expression;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface For extends BaseNode {
|
|
161
|
+
kind: 'For';
|
|
162
|
+
init?: VariableDeclaration | Assignment | ExpressionStatement;
|
|
163
|
+
condition?: Expression;
|
|
164
|
+
update?: Assignment | ExpressionStatement;
|
|
165
|
+
body: Block;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export type Statement =
|
|
169
|
+
VariableDeclaration | Assignment | ExpressionStatement | Return | Block | If | While | DoUntil | For;
|
|
170
|
+
|
|
171
|
+
export interface Program extends BaseNode {
|
|
172
|
+
kind: 'Program';
|
|
173
|
+
statements: Statement[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type Node = Program | Statement | Expression;
|
|
177
|
+
|
|
178
|
+
/** Hijos directos de un nodo, en el orden en que aparecen en el código. */
|
|
179
|
+
export const childrenOf = (node: Node): Node[] => {
|
|
180
|
+
switch (node.kind) {
|
|
181
|
+
case 'Program':
|
|
182
|
+
case 'Block':
|
|
183
|
+
return node.statements;
|
|
184
|
+
case 'ListLiteral':
|
|
185
|
+
return node.elements;
|
|
186
|
+
case 'Unary':
|
|
187
|
+
return [node.operand];
|
|
188
|
+
case 'Binary':
|
|
189
|
+
case 'Logical':
|
|
190
|
+
return [node.left, node.right];
|
|
191
|
+
case 'Call':
|
|
192
|
+
return [node.callee, ...node.args];
|
|
193
|
+
case 'Index':
|
|
194
|
+
return [node.object, node.index];
|
|
195
|
+
case 'FunctionLiteral':
|
|
196
|
+
return [...node.params, node.body];
|
|
197
|
+
case 'VariableDeclaration':
|
|
198
|
+
return [node.name, node.value];
|
|
199
|
+
case 'Assignment':
|
|
200
|
+
return [node.target, node.value];
|
|
201
|
+
case 'ExpressionStatement':
|
|
202
|
+
return [node.expression];
|
|
203
|
+
case 'Return':
|
|
204
|
+
return node.value ? [node.value] : [];
|
|
205
|
+
case 'If':
|
|
206
|
+
return [
|
|
207
|
+
...node.branches.flatMap(branch => [branch.condition, branch.body]),
|
|
208
|
+
...(node.alternative ? [node.alternative] : []),
|
|
209
|
+
];
|
|
210
|
+
case 'While':
|
|
211
|
+
return [node.condition, node.body];
|
|
212
|
+
case 'DoUntil':
|
|
213
|
+
return [node.body, node.condition];
|
|
214
|
+
case 'For':
|
|
215
|
+
return [
|
|
216
|
+
...(node.init ? [node.init] : []),
|
|
217
|
+
...(node.condition ? [node.condition] : []),
|
|
218
|
+
...(node.update ? [node.update] : []),
|
|
219
|
+
node.body,
|
|
220
|
+
];
|
|
221
|
+
default:
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/** Recorre el árbol en profundidad, del padre a los hijos. */
|
|
227
|
+
export const walk = (node: Node, visit: (node: Node, parent?: Node) => void, parent?: Node): void => {
|
|
228
|
+
visit(node, parent);
|
|
229
|
+
for (const child of childrenOf(node)) walk(child, visit, node);
|
|
230
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { CodexivoError } from '../errors';
|
|
2
|
+
import { KEYWORDS, TokenType } from './token';
|
|
3
|
+
import type { Position, Span, Token } from './token';
|
|
4
|
+
|
|
5
|
+
export interface LexResult {
|
|
6
|
+
tokens: Token[];
|
|
7
|
+
errors: CodexivoError[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const LETTER = /[A-Za-z_À-ÖØ-öø-ÿ]/;
|
|
11
|
+
const DIGIT = /[0-9]/;
|
|
12
|
+
const WHITESPACE = /\s/;
|
|
13
|
+
|
|
14
|
+
const TWO_CHAR_OPERATORS: Readonly<Record<string, TokenType>> = {
|
|
15
|
+
'==': TokenType.EQ,
|
|
16
|
+
'!=': TokenType.NEQ,
|
|
17
|
+
'<=': TokenType.LTE,
|
|
18
|
+
'>=': TokenType.GTE,
|
|
19
|
+
'+=': TokenType.PLUS_ASSIGN,
|
|
20
|
+
'-=': TokenType.MINUS_ASSIGN,
|
|
21
|
+
'*=': TokenType.STAR_ASSIGN,
|
|
22
|
+
'/=': TokenType.SLASH_ASSIGN,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ONE_CHAR_OPERATORS: Readonly<Record<string, TokenType>> = {
|
|
26
|
+
'=': TokenType.ASSIGN,
|
|
27
|
+
'+': TokenType.PLUS,
|
|
28
|
+
'-': TokenType.MINUS,
|
|
29
|
+
'*': TokenType.STAR,
|
|
30
|
+
'/': TokenType.SLASH,
|
|
31
|
+
'!': TokenType.BANG,
|
|
32
|
+
'<': TokenType.LT,
|
|
33
|
+
'>': TokenType.GT,
|
|
34
|
+
',': TokenType.COMMA,
|
|
35
|
+
';': TokenType.SEMICOLON,
|
|
36
|
+
'(': TokenType.LPAREN,
|
|
37
|
+
')': TokenType.RPAREN,
|
|
38
|
+
'{': TokenType.LBRACE,
|
|
39
|
+
'}': TokenType.RBRACE,
|
|
40
|
+
'[': TokenType.LBRACKET,
|
|
41
|
+
']': TokenType.RBRACKET,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const ESCAPES: Readonly<Record<string, string>> = {
|
|
45
|
+
n: '\n',
|
|
46
|
+
t: '\t',
|
|
47
|
+
'\\': '\\',
|
|
48
|
+
'"': '"',
|
|
49
|
+
"'": "'",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ILLEGAL_HINTS: Readonly<Record<string, string>> = {
|
|
53
|
+
'&': 'Para el «y» lógico escribe la palabra `y`.',
|
|
54
|
+
'|': 'Para el «o» lógico escribe la palabra `o`.',
|
|
55
|
+
'“': 'Parece una comilla tipográfica; usa comillas rectas `"`.',
|
|
56
|
+
'”': 'Parece una comilla tipográfica; usa comillas rectas `"`.',
|
|
57
|
+
'‘': "Parece una comilla tipográfica; usa comillas rectas `'`.",
|
|
58
|
+
'’': "Parece una comilla tipográfica; usa comillas rectas `'`.",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Convierte código fuente en tokens. Nunca lanza: los problemas se reportan en `errors`. */
|
|
62
|
+
export const tokenize = (source: string): LexResult => {
|
|
63
|
+
const tokens: Token[] = [];
|
|
64
|
+
const errors: CodexivoError[] = [];
|
|
65
|
+
|
|
66
|
+
let offset = 0;
|
|
67
|
+
let line = 1;
|
|
68
|
+
let column = 1;
|
|
69
|
+
|
|
70
|
+
const position = (): Position => ({ line, column, offset });
|
|
71
|
+
const peek = (ahead = 0): string => source[offset + ahead] ?? '';
|
|
72
|
+
const advance = (): string => {
|
|
73
|
+
const character = source[offset] ?? '';
|
|
74
|
+
offset += 1;
|
|
75
|
+
if (character === '\n') {
|
|
76
|
+
line += 1;
|
|
77
|
+
column = 1;
|
|
78
|
+
} else {
|
|
79
|
+
column += 1;
|
|
80
|
+
}
|
|
81
|
+
return character;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const push = (type: TokenType, start: Position, value?: number | string): Token => {
|
|
85
|
+
const token: Token = { type, lexeme: source.slice(start.offset, offset), span: { start, end: position() } };
|
|
86
|
+
if (value !== undefined) token.value = value;
|
|
87
|
+
tokens.push(token);
|
|
88
|
+
return token;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const lexicalError = (code: string, message: string, span: Span, hint?: string) => {
|
|
92
|
+
errors.push(new CodexivoError({ kind: 'léxico', code, message, hint, span }));
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
while (offset < source.length) {
|
|
96
|
+
const character = peek();
|
|
97
|
+
|
|
98
|
+
if (WHITESPACE.test(character)) {
|
|
99
|
+
advance();
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const start = position();
|
|
104
|
+
|
|
105
|
+
// comentarios
|
|
106
|
+
if (character === '/' && peek(1) === '/') {
|
|
107
|
+
while (offset < source.length && peek() !== '\n') advance();
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (character === '/' && peek(1) === '*') {
|
|
111
|
+
advance();
|
|
112
|
+
advance();
|
|
113
|
+
while (offset < source.length && !(peek() === '*' && peek(1) === '/')) advance();
|
|
114
|
+
if (offset >= source.length) {
|
|
115
|
+
lexicalError(
|
|
116
|
+
'COMENTARIO_SIN_CERRAR',
|
|
117
|
+
'El comentario que empieza aquí no se cerró.',
|
|
118
|
+
{
|
|
119
|
+
start,
|
|
120
|
+
end: { line: start.line, column: start.column + 2, offset: start.offset + 2 },
|
|
121
|
+
},
|
|
122
|
+
'Cierra el comentario con `*/`.',
|
|
123
|
+
);
|
|
124
|
+
} else {
|
|
125
|
+
advance();
|
|
126
|
+
advance();
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// números
|
|
132
|
+
if (DIGIT.test(character) || (character === '.' && DIGIT.test(peek(1)))) {
|
|
133
|
+
while (DIGIT.test(peek())) advance();
|
|
134
|
+
if (peek() === '.' && DIGIT.test(peek(1))) {
|
|
135
|
+
advance();
|
|
136
|
+
while (DIGIT.test(peek())) advance();
|
|
137
|
+
}
|
|
138
|
+
if (LETTER.test(peek())) {
|
|
139
|
+
while (LETTER.test(peek()) || DIGIT.test(peek())) advance();
|
|
140
|
+
const token = push(TokenType.ILLEGAL, start);
|
|
141
|
+
lexicalError(
|
|
142
|
+
'NOMBRE_EMPIEZA_CON_NÚMERO',
|
|
143
|
+
`\`${token.lexeme}\` no es un número ni un nombre válido.`,
|
|
144
|
+
token.span,
|
|
145
|
+
'Los nombres no pueden empezar con un dígito. Si es una multiplicación, escribe el operador: `2 * x`.',
|
|
146
|
+
);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
push(TokenType.NUMBER, start, Number(source.slice(start.offset, offset)));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// nombres y palabras reservadas
|
|
154
|
+
if (LETTER.test(character)) {
|
|
155
|
+
while (LETTER.test(peek()) || DIGIT.test(peek())) advance();
|
|
156
|
+
const word = source.slice(start.offset, offset);
|
|
157
|
+
push(KEYWORDS[word] ?? TokenType.IDENT, start);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// textos
|
|
162
|
+
if (character === '"' || character === "'") {
|
|
163
|
+
const quote = advance();
|
|
164
|
+
let value = '';
|
|
165
|
+
let closed = false;
|
|
166
|
+
while (offset < source.length && peek() !== '\n') {
|
|
167
|
+
const current = advance();
|
|
168
|
+
if (current === quote) {
|
|
169
|
+
closed = true;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
if (current === '\\') {
|
|
173
|
+
const escapeStart = { line, column: column - 1, offset: offset - 1 };
|
|
174
|
+
const escaped = advance();
|
|
175
|
+
const replacement = ESCAPES[escaped];
|
|
176
|
+
if (replacement === undefined) {
|
|
177
|
+
lexicalError(
|
|
178
|
+
'ESCAPE_DESCONOCIDO',
|
|
179
|
+
`\`\\${escaped}\` no es una secuencia de escape válida.`,
|
|
180
|
+
{ start: escapeStart, end: position() },
|
|
181
|
+
'Las secuencias válidas son `\\n`, `\\t`, `\\\\`, `\\"` y `\\\'`.',
|
|
182
|
+
);
|
|
183
|
+
value += escaped;
|
|
184
|
+
} else {
|
|
185
|
+
value += replacement;
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
value += current;
|
|
190
|
+
}
|
|
191
|
+
const token = push(TokenType.STRING, start, value);
|
|
192
|
+
if (!closed) {
|
|
193
|
+
lexicalError(
|
|
194
|
+
'TEXTO_SIN_CERRAR',
|
|
195
|
+
'El texto que empieza aquí no se cerró.',
|
|
196
|
+
token.span,
|
|
197
|
+
`Agrega la comilla \`${quote}\` al final. Un texto no puede ocupar varias líneas; usa \`\\n\` para un salto de línea.`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// operadores y delimitadores
|
|
204
|
+
const pair = character + peek(1);
|
|
205
|
+
const twoChar = TWO_CHAR_OPERATORS[pair];
|
|
206
|
+
if (twoChar) {
|
|
207
|
+
advance();
|
|
208
|
+
advance();
|
|
209
|
+
push(twoChar, start);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const oneChar = ONE_CHAR_OPERATORS[character];
|
|
213
|
+
if (oneChar) {
|
|
214
|
+
advance();
|
|
215
|
+
push(oneChar, start);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
advance();
|
|
220
|
+
const token = push(TokenType.ILLEGAL, start);
|
|
221
|
+
lexicalError(
|
|
222
|
+
'CARÁCTER_DESCONOCIDO',
|
|
223
|
+
`Codexivo no reconoce el carácter \`${character}\`.`,
|
|
224
|
+
token.span,
|
|
225
|
+
ILLEGAL_HINTS[character],
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const end = position();
|
|
230
|
+
tokens.push({ type: TokenType.EOF, lexeme: '', span: { start: end, end } });
|
|
231
|
+
return { tokens, errors };
|
|
232
|
+
};
|