nexicscript 0.1.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.
@@ -0,0 +1,29 @@
1
+ # Código de Conducta de NexicScript
2
+
3
+ ## Nuestro compromiso
4
+
5
+ Nos comprometemos a hacer de NexicScript una comunidad abierta y respetuosa para todos.
6
+
7
+ ## Comportamiento esperado
8
+
9
+ - Ser respetuoso y amable con todos
10
+ - Aceptar críticas constructivas
11
+ - Enfocarse en lo mejor para la comunidad
12
+ - Mostrar empatía hacia otros miembros
13
+
14
+ ## Comportamiento inaceptable
15
+
16
+ - Lenguaje ofensivo o insultos
17
+ - Acoso de cualquier tipo
18
+ - Publicar información privada de otros
19
+ - Cualquier conducta que se considere inapropiada
20
+
21
+ ## Aplicación
22
+
23
+ Los casos de comportamiento abusivo pueden ser reportados abriendo un Issue privado o contactando a los mantenedores del proyecto.
24
+
25
+ Los mantenedores tienen el derecho de eliminar comentarios, commits, o contribuciones que no respeten este código.
26
+
27
+ ## Atribución
28
+
29
+ Este Código de Conducta está adaptado del [Contributor Covenant](https://www.contributor-covenant.org), versión 2.1.
@@ -0,0 +1,39 @@
1
+ # Cómo contribuir a NexicScript
2
+
3
+ ¡Gracias por querer contribuir! NexicScript es un proyecto abierto y toda ayuda es bienvenida.
4
+
5
+ ## Formas de contribuir
6
+
7
+ - 🐛 Reportar bugs abriendo un Issue
8
+ - 💡 Proponer nuevas funcionalidades
9
+ - 🔧 Enviar Pull Requests con mejoras
10
+ - 📖 Mejorar la documentación
11
+ - 🌍 Traducir documentación
12
+
13
+ ## Proceso para contribuir
14
+
15
+ 1. Haz fork del repositorio
16
+ 2. Crea una rama: git checkout -b mi-mejora
17
+ 3. Haz tus cambios
18
+ 4. Haz commit: git commit -m "Descripción clara del cambio"
19
+ 5. Haz push: git push origin mi-mejora
20
+ 6. Abre un Pull Request
21
+
22
+ ## Estilo de código
23
+
24
+ - Usa 2 espacios para indentar
25
+ - Nombres de variables en camelCase
26
+ - Comentarios en español o inglés
27
+ - Prueba tus cambios con los ejemplos en /ejemplos
28
+
29
+ ## Reportar bugs
30
+
31
+ Abre un Issue con:
32
+ - Versión de NexicScript
33
+ - Sistema operativo
34
+ - Código .nxs que produce el error
35
+ - Mensaje de error completo
36
+
37
+ ## Código de conducta
38
+
39
+ Lee nuestro [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) antes de contribuir.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NexicScript
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # NexicScript
2
+
3
+ > Lenguaje de programación minimalista para bots, web y bases de datos.
4
+
5
+ ```nxs
6
+ fn saludar(nombre)
7
+ "Hola, " + nombre
8
+
9
+ print(saludar("Mundo"))
10
+ ```
11
+
12
+ ## Instalación
13
+
14
+ ```bash
15
+ npm install -g nexicscript
16
+ ```
17
+
18
+ ## Uso
19
+
20
+ ```bash
21
+ nexic mi_programa.nxs
22
+ ```
23
+
24
+ ## Características
25
+
26
+ - Sin punto y coma
27
+ - Sin llaves — bloques por indentación
28
+ - Módulos integrados: `discord`, `web`, `db`
29
+ - Sin dependencias externas para proyectos simples
30
+ - Archivos `.nxs`
31
+
32
+ ## Ejemplos
33
+
34
+ ```nxs
35
+ // Variables
36
+ let nombre = "Bot"
37
+ const puerto = 8080
38
+
39
+ // Funciones
40
+ fn sumar(a, b)
41
+ a + b
42
+
43
+ // Listas y objetos
44
+ let usuarios = ["Ana", "Luis"]
45
+ let config = { puerto: 8080, debug: true }
46
+
47
+ // Discord bot
48
+ import discord
49
+ discord.conectar("TOKEN")
50
+ discord.on("mensaje") fn(msg)
51
+ if msg.contenido == "!hola"
52
+ discord.enviar(msg.canal, "Hola!")
53
+
54
+ // Servidor web
55
+ import web
56
+ let srv = web.crearServidor fn(req)
57
+ web.responder(req, 200, "OK")
58
+ srv.iniciar(8080)
59
+
60
+ // Base de datos
61
+ import db
62
+ db.conectar("datos.db")
63
+ db.insertar("usuarios", { nombre: "Ana", edad: 28 })
64
+ ```
65
+
66
+ ## Licencia
67
+
68
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,27 @@
1
+ # Seguridad de NexicScript
2
+
3
+ ## Reportar una vulnerabilidad
4
+
5
+ Si encuentras una vulnerabilidad de seguridad en NexicScript, **NO** abras un Issue público.
6
+
7
+ En su lugar, repórtala de forma privada a:
8
+ - Email: security@nexicscript.dev
9
+ - O usa GitHub Private Vulnerability Reporting
10
+
11
+ ## Qué reportar
12
+
13
+ - Errores en el intérprete que permitan ejecutar código malicioso
14
+ - Vulnerabilidades en los módulos integrados (discord, web, db)
15
+ - Cualquier comportamiento inesperado que comprometa la seguridad
16
+
17
+ ## Tiempo de respuesta
18
+
19
+ Nos comprometemos a responder en menos de 72 horas.
20
+
21
+ ## Versiones soportadas
22
+
23
+ | Versión | Soporte de seguridad |
24
+ |---------|----------------------|
25
+ | 0.1.x | ✅ Activa |
26
+
27
+ Gracias por ayudar a mantener NexicScript seguro.
package/bin/nexic.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ const fs = require('fs')
5
+ const path = require('path')
6
+ const { ejecutarCodigo } = require('../src/index')
7
+
8
+ const args = process.argv.slice(2)
9
+
10
+ if (args.length === 0) {
11
+ console.log(`
12
+ ███╗ ██╗███████╗██╗ ██╗██╗ ██████╗███████╗ ██████╗██████╗ ██╗██████╗ ████████╗
13
+ ████╗ ██║██╔════╝╚██╗██╔╝██║██╔════╝██╔════╝██╔════╝██╔══██╗██║██╔══██╗╚══██╔══╝
14
+ ██╔██╗ ██║█████╗ ╚███╔╝ ██║██║ ███████╗██║ ██████╔╝██║██████╔╝ ██║
15
+ ██║╚██╗██║██╔══╝ ██╔██╗ ██║██║ ╚════██║██║ ██╔══██╗██║██╔═══╝ ██║
16
+ ██║ ╚████║███████╗██╔╝ ██╗██║╚██████╗███████║╚██████╗██║ ██║██║██║ ██║
17
+ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝
18
+
19
+ NexicScript v0.1.0
20
+ Uso: nexic <archivo.nxs>
21
+ `)
22
+ process.exit(0)
23
+ }
24
+
25
+ const archivo = args[0]
26
+
27
+ if (!archivo.endsWith('.nxs')) {
28
+ console.error('[NexicScript] Los archivos deben tener extensión .nxs')
29
+ process.exit(1)
30
+ }
31
+
32
+ if (!fs.existsSync(archivo)) {
33
+ console.error(`[NexicScript] Archivo no encontrado: ${archivo}`)
34
+ process.exit(1)
35
+ }
36
+
37
+ const codigo = fs.readFileSync(archivo, 'utf8')
38
+
39
+ try {
40
+ ejecutarCodigo(codigo)
41
+ } catch (e) {
42
+ console.error(e.message)
43
+ process.exit(1)
44
+ }
@@ -0,0 +1,13 @@
1
+ // Bot de Discord en NexicScript
2
+ import discord
3
+
4
+ discord.conectar("TU_TOKEN_AQUI")
5
+
6
+ discord.on("mensaje") fn(msg)
7
+ if msg.contenido == "!hola"
8
+ discord.enviar(msg.canal, "Hola! Soy un bot hecho con NexicScript")
9
+
10
+ if msg.contenido == "!version"
11
+ discord.enviar(msg.canal, "NexicScript v0.1.0")
12
+
13
+ print("Bot iniciado y escuchando...")
@@ -0,0 +1,45 @@
1
+ // Ejemplo básico de NexicScript
2
+ let nombre = "NexicScript"
3
+ const version = "0.1.0"
4
+
5
+ fn saludar(quien)
6
+ "Hola desde " + quien + " v" + version
7
+
8
+ print(saludar(nombre))
9
+
10
+ // Listas y bucles
11
+ let frutas = ["manzana", "pera", "uva"]
12
+
13
+ for fruta in frutas
14
+ print("Fruta: " + fruta)
15
+
16
+ // Condicional
17
+ let x = 42
18
+
19
+ if x > 100
20
+ print("Grande")
21
+ else if x > 10
22
+ print("Mediano: " + x)
23
+ else
24
+ print("Pequeño")
25
+
26
+ // Objetos
27
+ let usuario = {
28
+ nombre: "Ana"
29
+ edad: 28
30
+ activo: true
31
+ }
32
+
33
+ print("Usuario: " + usuario.nombre + ", edad: " + usuario.edad)
34
+
35
+ // Try / catch
36
+ fn dividir(a, b)
37
+ if b == 0
38
+ throw "No se puede dividir entre cero"
39
+ a / b
40
+
41
+ try
42
+ print("10 / 2 = " + dividir(10, 2))
43
+ print("10 / 0 = " + dividir(10, 0))
44
+ catch error
45
+ print("Error atrapado: " + error)
@@ -0,0 +1,17 @@
1
+ // Servidor web en NexicScript
2
+ import web
3
+ import db
4
+
5
+ db.conectar("datos.db")
6
+
7
+ let manejador = fn(req)
8
+ if req.ruta == "/"
9
+ web.responder(req, 200, "Bienvenido a NexicScript")
10
+ else if req.ruta == "/usuarios"
11
+ let lista = db.consultar("usuarios", fn(u) true)
12
+ web.responder(req, 200, lista)
13
+ else
14
+ web.responder(req, 404, "Página no encontrada")
15
+
16
+ let servidor = web.crearServidor(manejador)
17
+ servidor.iniciar(8080)
package/icon.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "nexicscript",
3
+ "version": "0.1.0",
4
+ "description": "NexicScript - Lenguaje de programación minimalista para bots, web y bases de datos",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "nexic": "./bin/nexic.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node bin/nexic.js ejemplos/hola.nxs"
11
+ },
12
+ "keywords": ["nexicscript", "lenguaje", "discord", "web", "db"],
13
+ "author": "NexicScript Team",
14
+ "license": "MIT"
15
+ }
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ 'use strict'
2
+
3
+ const { lexer } = require('./lexer')
4
+ const { parsear } = require('./parser')
5
+ const { interpretar } = require('./interpreter')
6
+
7
+ function ejecutarCodigo(codigo) {
8
+ const tokens = lexer(codigo)
9
+ const ast = parsear(tokens)
10
+ return interpretar(ast)
11
+ }
12
+
13
+ module.exports = { ejecutarCodigo }
@@ -0,0 +1,334 @@
1
+ 'use strict'
2
+
3
+ class ReturnSignal { constructor(v) { this.valor = v } }
4
+ class ThrowSignal { constructor(v) { this.valor = v } }
5
+
6
+ class Entorno {
7
+ constructor(padre = null) {
8
+ this.vars = new Map()
9
+ this.padre = padre
10
+ }
11
+
12
+ obtener(nombre) {
13
+ if (this.vars.has(nombre)) return this.vars.get(nombre)
14
+ if (this.padre) return this.padre.obtener(nombre)
15
+ throw new Error(`[NexicScript] Variable '${nombre}' no definida`)
16
+ }
17
+
18
+ definir(nombre, valor) {
19
+ this.vars.set(nombre, valor)
20
+ }
21
+
22
+ asignar(nombre, valor) {
23
+ if (this.vars.has(nombre)) { this.vars.set(nombre, valor); return }
24
+ if (this.padre) { this.padre.asignar(nombre, valor); return }
25
+ throw new Error(`[NexicScript] Variable '${nombre}' no definida`)
26
+ }
27
+ }
28
+
29
+ class Funcion {
30
+ constructor(nombre, params, cuerpo, entorno) {
31
+ this.nombre = nombre
32
+ this.params = params
33
+ this.cuerpo = cuerpo
34
+ this.entorno = entorno
35
+ }
36
+ }
37
+
38
+ function crearEntornoGlobal() {
39
+ const env = new Entorno()
40
+
41
+ // Funciones nativas
42
+ env.definir('print', (...args) => { console.log(...args.map(nxsStr)); return null })
43
+ env.definir('largo', (v) => v.length ?? 0)
44
+ env.definir('texto', (v) => nxsStr(v))
45
+ env.definir('numero', (v) => Number(v))
46
+ env.definir('tipo', (v) => typeof v)
47
+
48
+ // Módulo discord (simulado — conectar con discord.js en producción)
49
+ env.definir('discord', {
50
+ _listeners: {},
51
+ conectar(token) { console.log(`[discord] Conectando con token: ${token.slice(0,8)}...`) },
52
+ on(evento, fn) { this._listeners[evento] = fn },
53
+ enviar(canal, mensaje) { console.log(`[discord] → #${canal}: ${mensaje}`) },
54
+ _simular(evento, datos) {
55
+ if (this._listeners[evento]) this._listeners[evento](datos)
56
+ }
57
+ })
58
+
59
+ // Módulo web (simulado — conectar con http en producción)
60
+ env.definir('web', {
61
+ crearServidor(manejador) {
62
+ const srv = {
63
+ _manejador: manejador,
64
+ iniciar: function(puerto) { console.log(`[web] Servidor escuchando en puerto ${puerto}`) },
65
+ _simular: function(ruta) {
66
+ const req = { ruta }
67
+ this._manejador(req)
68
+ }
69
+ }
70
+ return srv
71
+ },
72
+ responder(req, codigo, cuerpo) {
73
+ console.log(`[web] ${codigo} ${req.ruta} → ${nxsStr(cuerpo)}`)
74
+ }
75
+ })
76
+
77
+ // Módulo db (simulado — conectar con SQLite/PostgreSQL en producción)
78
+ env.definir('db', {
79
+ _datos: {},
80
+ _url: null,
81
+ conectar(url) { this._url = url; console.log(`[db] Conectado a: ${url}`) },
82
+ insertar(tabla, obj) {
83
+ if (!this._datos[tabla]) this._datos[tabla] = []
84
+ this._datos[tabla].push({ ...obj })
85
+ console.log(`[db] Insertado en '${tabla}':`, obj)
86
+ },
87
+ consultar(tabla, filtro) {
88
+ const rows = this._datos[tabla] || []
89
+ return typeof filtro === 'function' ? rows.filter(filtro) : rows
90
+ },
91
+ actualizar(tabla, filtro, cambios) {
92
+ const rows = this._datos[tabla] || []
93
+ rows.forEach((r, i) => { if (filtro(r)) this._datos[tabla][i] = { ...r, ...cambios } })
94
+ },
95
+ eliminar(tabla, filtro) {
96
+ this._datos[tabla] = (this._datos[tabla] || []).filter(r => !filtro(r))
97
+ }
98
+ })
99
+
100
+ return env
101
+ }
102
+
103
+ function nxsStr(v) {
104
+ if (v === null || v === undefined) return 'null'
105
+ if (typeof v === 'object' && !Array.isArray(v)) return JSON.stringify(v)
106
+ if (Array.isArray(v)) return '[' + v.map(nxsStr).join(', ') + ']'
107
+ return String(v)
108
+ }
109
+
110
+ function ejecutarBloque(stmts, env) {
111
+ let ultimo = null
112
+ for (const s of stmts) {
113
+ ultimo = ejecutar(s, env)
114
+ if (ultimo instanceof ReturnSignal || ultimo instanceof ThrowSignal) return ultimo
115
+ }
116
+ return ultimo
117
+ }
118
+
119
+ function ejecutar(nodo, env) {
120
+ if (!nodo) return null
121
+
122
+ switch (nodo.tipo) {
123
+ case 'Programa':
124
+ return ejecutarBloque(nodo.cuerpo, env)
125
+
126
+ case 'DeclVar': {
127
+ const val = evaluar(nodo.valor, env)
128
+ env.definir(nodo.nombre, val)
129
+ return null
130
+ }
131
+
132
+ case 'DeclFn': {
133
+ const fn = new Funcion(nodo.nombre, nodo.params, nodo.cuerpo, env)
134
+ if (nodo.nombre) env.definir(nodo.nombre, fn)
135
+ return fn
136
+ }
137
+
138
+ case 'Export': {
139
+ const resultado = ejecutar(nodo.decl, env)
140
+ if (nodo.decl.nombre) env.definir('__export__' + nodo.decl.nombre, resultado)
141
+ return resultado
142
+ }
143
+
144
+ case 'Import': {
145
+ // Módulos integrados ya están en el entorno global
146
+ // Para módulos propios se extendería aquí
147
+ try {
148
+ const modulo = env.obtener(nodo.nombre)
149
+ return modulo
150
+ } catch {
151
+ throw new Error(`[NexicScript] Módulo '${nodo.nombre}' no encontrado`)
152
+ }
153
+ }
154
+
155
+ case 'ExprStmt':
156
+ return evaluar(nodo.expr, env)
157
+
158
+ case 'If': {
159
+ const cond = evaluar(nodo.condicion, env)
160
+ if (cond) {
161
+ return ejecutarBloque(nodo.entonces, env)
162
+ } else if (nodo.sino) {
163
+ if (nodo.sino.tipo === 'If') return ejecutar(nodo.sino, env)
164
+ return ejecutarBloque(nodo.sino.cuerpo, env)
165
+ }
166
+ return null
167
+ }
168
+
169
+ case 'While': {
170
+ let res = null
171
+ while (evaluar(nodo.condicion, env)) {
172
+ res = ejecutarBloque(nodo.cuerpo, env)
173
+ if (res instanceof ReturnSignal || res instanceof ThrowSignal) return res
174
+ }
175
+ return res
176
+ }
177
+
178
+ case 'For': {
179
+ const lista = evaluar(nodo.lista, env)
180
+ let res = null
181
+ for (const item of lista) {
182
+ const loopEnv = new Entorno(env)
183
+ loopEnv.definir(nodo.variable, item)
184
+ res = ejecutarBloque(nodo.cuerpo, loopEnv)
185
+ if (res instanceof ReturnSignal || res instanceof ThrowSignal) return res
186
+ }
187
+ return res
188
+ }
189
+
190
+ case 'Try': {
191
+ try {
192
+ const res = ejecutarBloque(nodo.intentar, env)
193
+ if (res instanceof ThrowSignal) {
194
+ const catchEnv = new Entorno(env)
195
+ catchEnv.definir(nodo.errorVar, res.valor)
196
+ return ejecutarBloque(nodo.capturar, catchEnv)
197
+ }
198
+ return res
199
+ } catch (e) {
200
+ const catchEnv = new Entorno(env)
201
+ catchEnv.definir(nodo.errorVar, e.message)
202
+ return ejecutarBloque(nodo.capturar, catchEnv)
203
+ }
204
+ }
205
+
206
+ case 'Throw':
207
+ return new ThrowSignal(evaluar(nodo.valor, env))
208
+
209
+ case 'Return':
210
+ return new ReturnSignal(nodo.valor ? evaluar(nodo.valor, env) : null)
211
+
212
+ default:
213
+ return evaluar(nodo, env)
214
+ }
215
+ }
216
+
217
+ function evaluar(nodo, env) {
218
+ switch (nodo.tipo) {
219
+ case 'Numero': return nodo.valor
220
+ case 'Texto': return nodo.valor
221
+ case 'Booleano': return nodo.valor
222
+ case 'Nulo': return null
223
+
224
+ case 'Identificador':
225
+ return env.obtener(nodo.nombre)
226
+
227
+ case 'Lista':
228
+ return nodo.elementos.map(e => evaluar(e, env))
229
+
230
+ case 'Objeto': {
231
+ const obj = {}
232
+ for (const { clave, valor } of nodo.pares) obj[clave] = evaluar(valor, env)
233
+ return obj
234
+ }
235
+
236
+ case 'Acceso': {
237
+ const obj = evaluar(nodo.objeto, env)
238
+ if (obj === null || obj === undefined)
239
+ throw new Error(`[NexicScript] No se puede acceder a '${nodo.propiedad}' de null`)
240
+ const val = obj[nodo.propiedad]
241
+ if (typeof val === 'function') return val.bind(obj)
242
+ if (val instanceof Funcion) return val
243
+ return val
244
+ }
245
+
246
+ case 'Indice': {
247
+ const obj = evaluar(nodo.objeto, env)
248
+ const idx = evaluar(nodo.indice, env)
249
+ return obj[idx]
250
+ }
251
+
252
+ case 'BinOp': {
253
+ const i = evaluar(nodo.izq, env)
254
+ const d = evaluar(nodo.der, env)
255
+ switch (nodo.op) {
256
+ case '+': return typeof i === 'string' || typeof d === 'string' ? nxsStr(i) + nxsStr(d) : i + d
257
+ case '-': return i - d
258
+ case '*': return i * d
259
+ case '/': return i / d
260
+ case '==': return i === d
261
+ case '!=': return i !== d
262
+ case '>': return i > d
263
+ case '<': return i < d
264
+ case '>=': return i >= d
265
+ case '<=': return i <= d
266
+ case 'and': return i && d
267
+ case 'or': return i || d
268
+ }
269
+ }
270
+
271
+ case 'Unario': {
272
+ const v = evaluar(nodo.valor, env)
273
+ if (nodo.op === '-') return -v
274
+ if (nodo.op === 'not') return !v
275
+ }
276
+
277
+ case 'Asignacion': {
278
+ const val = evaluar(nodo.valor, env)
279
+ if (nodo.objetivo.tipo === 'Identificador') {
280
+ env.asignar(nodo.objetivo.nombre, val)
281
+ } else if (nodo.objetivo.tipo === 'Acceso') {
282
+ const obj = evaluar(nodo.objetivo.objeto, env)
283
+ obj[nodo.objetivo.propiedad] = val
284
+ }
285
+ return val
286
+ }
287
+
288
+ case 'Llamada': {
289
+ const fn = evaluar(nodo.fn, env)
290
+ const args = nodo.args.map(a => evaluar(a, env))
291
+
292
+ if (typeof fn === 'function') {
293
+ // Wrap any NexicScript Funcion args as JS callables for native modules
294
+ const jsArgs = args.map(a => {
295
+ if (a instanceof Funcion) {
296
+ return (...innerArgs) => {
297
+ const fnEnv = new Entorno(a.entorno)
298
+ a.params.forEach((p, i) => fnEnv.definir(p, innerArgs[i] ?? null))
299
+ const res = ejecutarBloque(a.cuerpo, fnEnv)
300
+ if (res instanceof ReturnSignal) return res.valor
301
+ return res
302
+ }
303
+ }
304
+ return a
305
+ })
306
+ return fn(...jsArgs)
307
+ }
308
+
309
+ if (fn instanceof Funcion) {
310
+ const fnEnv = new Entorno(fn.entorno)
311
+ fn.params.forEach((p, i) => fnEnv.definir(p, args[i] ?? null))
312
+ const res = ejecutarBloque(fn.cuerpo, fnEnv)
313
+ if (res instanceof ReturnSignal) return res.valor
314
+ if (res instanceof ThrowSignal) throw new Error(nxsStr(res.valor))
315
+ return res
316
+ }
317
+
318
+ throw new Error(`[NexicScript] '${nodo.fn.nombre || nodo.fn.propiedad}' no es una función`)
319
+ }
320
+
321
+ case 'DeclFn':
322
+ return ejecutar(nodo, env)
323
+
324
+ default:
325
+ return ejecutar(nodo, env)
326
+ }
327
+ }
328
+
329
+ function interpretar(ast) {
330
+ const env = crearEntornoGlobal()
331
+ return ejecutar(ast, env)
332
+ }
333
+
334
+ module.exports = { interpretar }
package/src/lexer.js ADDED
@@ -0,0 +1,159 @@
1
+ 'use strict'
2
+
3
+ const PALABRAS_CLAVE = new Set([
4
+ 'let', 'const', 'fn', 'import', 'export', 'from',
5
+ 'if', 'else', 'for', 'in', 'while',
6
+ 'try', 'catch', 'throw', 'return',
7
+ 'true', 'false', 'null'
8
+ ])
9
+
10
+ const TIPOS = {
11
+ // Literales
12
+ NUMERO: 'NUMERO',
13
+ TEXTO: 'TEXTO',
14
+ BOOLEANO: 'BOOLEANO',
15
+ NULO: 'NULO',
16
+ // Identificadores
17
+ IDENTIFICADOR:'IDENTIFICADOR',
18
+ PALABRA_CLAVE:'PALABRA_CLAVE',
19
+ // Operadores
20
+ IGUAL: 'IGUAL', // =
21
+ IGUAL_IGUAL: 'IGUAL_IGUAL', // ==
22
+ NO_IGUAL: 'NO_IGUAL', // !=
23
+ MAYOR: 'MAYOR', // >
24
+ MENOR: 'MENOR', // <
25
+ MAYOR_IGUAL: 'MAYOR_IGUAL', // >=
26
+ MENOR_IGUAL: 'MENOR_IGUAL', // <=
27
+ MAS: 'MAS', // +
28
+ MENOS: 'MENOS', // -
29
+ POR: 'POR', // *
30
+ ENTRE: 'ENTRE', // /
31
+ Y: 'Y', // and
32
+ O: 'O', // or
33
+ NO: 'NO', // not
34
+ // Puntuación
35
+ PAREN_IZQ: 'PAREN_IZQ', // (
36
+ PAREN_DER: 'PAREN_DER', // )
37
+ LLAVE_IZQ: 'LLAVE_IZQ', // {
38
+ LLAVE_DER: 'LLAVE_DER', // }
39
+ CORCHETE_IZQ: 'CORCHETE_IZQ', // [
40
+ CORCHETE_DER: 'CORCHETE_DER', // ]
41
+ COMA: 'COMA', // ,
42
+ PUNTO: 'PUNTO', // .
43
+ DOS_PUNTOS: 'DOS_PUNTOS', // :
44
+ // Estructura
45
+ NUEVA_LINEA: 'NUEVA_LINEA',
46
+ INDENTAR: 'INDENTAR', // bloque entra
47
+ DESINDENTAR: 'DESINDENTAR', // bloque sale
48
+ FIN: 'FIN',
49
+ }
50
+
51
+ function lexer(codigo) {
52
+ const tokens = []
53
+ const lineas = codigo.split('\n')
54
+ const pilaIndentacion = [0]
55
+
56
+ for (let numLinea = 0; numLinea < lineas.length; numLinea++) {
57
+ const linea = lineas[numLinea]
58
+
59
+ // Ignorar líneas vacías y comentarios
60
+ const lineaSinEspacios = linea.trimEnd()
61
+ if (lineaSinEspacios === '' || lineaSinEspacios.trimStart().startsWith('//')) continue
62
+
63
+ // Contar espacios al inicio
64
+ let espacios = 0
65
+ while (espacios < linea.length && linea[espacios] === ' ') espacios++
66
+
67
+ const indentActual = pilaIndentacion[pilaIndentacion.length - 1]
68
+
69
+ if (espacios > indentActual) {
70
+ pilaIndentacion.push(espacios)
71
+ tokens.push({ tipo: TIPOS.INDENTAR, valor: null, linea: numLinea + 1 })
72
+ } else {
73
+ while (espacios < pilaIndentacion[pilaIndentacion.length - 1]) {
74
+ pilaIndentacion.pop()
75
+ tokens.push({ tipo: TIPOS.DESINDENTAR, valor: null, linea: numLinea + 1 })
76
+ }
77
+ }
78
+
79
+ // Tokenizar la línea
80
+ let i = espacios
81
+ while (i < linea.length) {
82
+ // Saltar espacios
83
+ if (linea[i] === ' ') { i++; continue }
84
+
85
+ // Comentario inline
86
+ if (linea[i] === '/' && linea[i+1] === '/') break
87
+
88
+ // Número
89
+ if (/[0-9]/.test(linea[i])) {
90
+ let num = ''
91
+ while (i < linea.length && /[0-9.]/.test(linea[i])) num += linea[i++]
92
+ tokens.push({ tipo: TIPOS.NUMERO, valor: parseFloat(num), linea: numLinea + 1 })
93
+ continue
94
+ }
95
+
96
+ // Texto con comillas dobles o simples
97
+ if (linea[i] === '"' || linea[i] === "'") {
98
+ const comilla = linea[i++]
99
+ let texto = ''
100
+ while (i < linea.length && linea[i] !== comilla) texto += linea[i++]
101
+ i++ // cerrar comilla
102
+ tokens.push({ tipo: TIPOS.TEXTO, valor: texto, linea: numLinea + 1 })
103
+ continue
104
+ }
105
+
106
+ // Identificador o palabra clave
107
+ if (/[a-zA-Z_]/.test(linea[i])) {
108
+ let nombre = ''
109
+ while (i < linea.length && /[a-zA-Z0-9_]/.test(linea[i])) nombre += linea[i++]
110
+ if (nombre === 'true' || nombre === 'false') {
111
+ tokens.push({ tipo: TIPOS.BOOLEANO, valor: nombre === 'true', linea: numLinea + 1 })
112
+ } else if (nombre === 'null') {
113
+ tokens.push({ tipo: TIPOS.NULO, valor: null, linea: numLinea + 1 })
114
+ } else if (PALABRAS_CLAVE.has(nombre)) {
115
+ tokens.push({ tipo: TIPOS.PALABRA_CLAVE, valor: nombre, linea: numLinea + 1 })
116
+ } else {
117
+ tokens.push({ tipo: TIPOS.IDENTIFICADOR, valor: nombre, linea: numLinea + 1 })
118
+ }
119
+ continue
120
+ }
121
+
122
+ // Operadores y puntuación
123
+ const doble = linea.slice(i, i+2)
124
+ if (doble === '==') { tokens.push({ tipo: TIPOS.IGUAL_IGUAL, valor: '==', linea: numLinea + 1 }); i+=2; continue }
125
+ if (doble === '!=') { tokens.push({ tipo: TIPOS.NO_IGUAL, valor: '!=', linea: numLinea + 1 }); i+=2; continue }
126
+ if (doble === '>=') { tokens.push({ tipo: TIPOS.MAYOR_IGUAL, valor: '>=', linea: numLinea + 1 }); i+=2; continue }
127
+ if (doble === '<=') { tokens.push({ tipo: TIPOS.MENOR_IGUAL, valor: '<=', linea: numLinea + 1 }); i+=2; continue }
128
+
129
+ const simple = linea[i]
130
+ const mapaSimple = {
131
+ '=': TIPOS.IGUAL, '>': TIPOS.MAYOR, '<': TIPOS.MENOR,
132
+ '+': TIPOS.MAS, '-': TIPOS.MENOS, '*': TIPOS.POR, '/': TIPOS.ENTRE,
133
+ '(': TIPOS.PAREN_IZQ, ')': TIPOS.PAREN_DER,
134
+ '{': TIPOS.LLAVE_IZQ, '}': TIPOS.LLAVE_DER,
135
+ '[': TIPOS.CORCHETE_IZQ, ']': TIPOS.CORCHETE_DER,
136
+ ',': TIPOS.COMA, '.': TIPOS.PUNTO, ':': TIPOS.DOS_PUNTOS,
137
+ }
138
+ if (mapaSimple[simple]) {
139
+ tokens.push({ tipo: mapaSimple[simple], valor: simple, linea: numLinea + 1 })
140
+ i++; continue
141
+ }
142
+
143
+ throw new Error(`[NexicScript] Carácter desconocido '${linea[i]}' en línea ${numLinea + 1}`)
144
+ }
145
+
146
+ tokens.push({ tipo: TIPOS.NUEVA_LINEA, valor: null, linea: numLinea + 1 })
147
+ }
148
+
149
+ // Cerrar indentaciones abiertas
150
+ while (pilaIndentacion.length > 1) {
151
+ pilaIndentacion.pop()
152
+ tokens.push({ tipo: TIPOS.DESINDENTAR, valor: null, linea: -1 })
153
+ }
154
+
155
+ tokens.push({ tipo: TIPOS.FIN, valor: null, linea: -1 })
156
+ return tokens
157
+ }
158
+
159
+ module.exports = { lexer, TIPOS }
package/src/parser.js ADDED
@@ -0,0 +1,352 @@
1
+ 'use strict'
2
+
3
+ const { TIPOS } = require('./lexer')
4
+
5
+ class Parser {
6
+ constructor(tokens) {
7
+ this.tokens = tokens
8
+ this.pos = 0
9
+ }
10
+
11
+ actual() { return this.tokens[this.pos] }
12
+ siguiente() { return this.tokens[this.pos + 1] }
13
+
14
+ consumir(tipo, valor) {
15
+ const t = this.actual()
16
+ if (tipo && t.tipo !== tipo) {
17
+ throw new Error(`[NexicScript] Se esperaba ${tipo} pero se encontró ${t.tipo} ('${t.valor}') en línea ${t.linea}`)
18
+ }
19
+ if (valor !== undefined && t.valor !== valor) {
20
+ throw new Error(`[NexicScript] Se esperaba '${valor}' pero se encontró '${t.valor}' en línea ${t.linea}`)
21
+ }
22
+ this.pos++
23
+ return t
24
+ }
25
+
26
+ saltarLineas() {
27
+ while (this.actual().tipo === TIPOS.NUEVA_LINEA) this.pos++
28
+ }
29
+
30
+ // Salta líneas nuevas, INDENTAR y DESINDENTAR (para dentro de {} y [])
31
+ saltarEstructura() {
32
+ while ([TIPOS.NUEVA_LINEA, TIPOS.INDENTAR, TIPOS.DESINDENTAR].includes(this.actual().tipo)) this.pos++
33
+ }
34
+
35
+ parsear() {
36
+ const stmts = []
37
+ this.saltarLineas()
38
+ while (this.actual().tipo !== TIPOS.FIN) {
39
+ stmts.push(this.stmt())
40
+ this.saltarLineas()
41
+ }
42
+ return { tipo: 'Programa', cuerpo: stmts }
43
+ }
44
+
45
+ stmt() {
46
+ const t = this.actual()
47
+
48
+ if (t.tipo === TIPOS.PALABRA_CLAVE) {
49
+ switch (t.valor) {
50
+ case 'let': return this.declVar(false)
51
+ case 'const': return this.declVar(true)
52
+ case 'fn': return this.declFn()
53
+ case 'export': return this.declExport()
54
+ case 'import': return this.declImport()
55
+ case 'if': return this.declIf()
56
+ case 'while': return this.declWhile()
57
+ case 'for': return this.declFor()
58
+ case 'try': return this.declTry()
59
+ case 'throw': return this.declThrow()
60
+ case 'return': return this.declReturn()
61
+ }
62
+ }
63
+
64
+ return this.exprStmt()
65
+ }
66
+
67
+ declVar(esConst) {
68
+ this.consumir(TIPOS.PALABRA_CLAVE)
69
+ const nombre = this.consumir(TIPOS.IDENTIFICADOR).valor
70
+ this.consumir(TIPOS.IGUAL)
71
+ const valor = this.expr()
72
+ this.saltarLineas()
73
+ return { tipo: 'DeclVar', nombre, valor, esConst }
74
+ }
75
+
76
+ declFn() {
77
+ this.consumir(TIPOS.PALABRA_CLAVE, 'fn')
78
+ const nombre = this.actual().tipo === TIPOS.IDENTIFICADOR ? this.consumir(TIPOS.IDENTIFICADOR).valor : null
79
+ this.consumir(TIPOS.PAREN_IZQ)
80
+ const params = []
81
+ while (this.actual().tipo !== TIPOS.PAREN_DER) {
82
+ params.push(this.consumir(TIPOS.IDENTIFICADOR).valor)
83
+ if (this.actual().tipo === TIPOS.COMA) this.consumir(TIPOS.COMA)
84
+ }
85
+ this.consumir(TIPOS.PAREN_DER)
86
+ const cuerpo = this.bloque()
87
+ return { tipo: 'DeclFn', nombre, params, cuerpo }
88
+ }
89
+
90
+ declExport() {
91
+ this.consumir(TIPOS.PALABRA_CLAVE, 'export')
92
+ const decl = this.stmt()
93
+ return { tipo: 'Export', decl }
94
+ }
95
+
96
+ declImport() {
97
+ this.consumir(TIPOS.PALABRA_CLAVE, 'import')
98
+ const nombre = this.consumir(TIPOS.IDENTIFICADOR).valor
99
+ let desde = null
100
+ if (this.actual().tipo === TIPOS.PALABRA_CLAVE && this.actual().valor === 'from') {
101
+ this.consumir(TIPOS.PALABRA_CLAVE, 'from')
102
+ desde = this.consumir(TIPOS.TEXTO).valor
103
+ }
104
+ this.saltarLineas()
105
+ return { tipo: 'Import', nombre, desde }
106
+ }
107
+
108
+ declIf() {
109
+ this.consumir(TIPOS.PALABRA_CLAVE, 'if')
110
+ const condicion = this.expr()
111
+ const entonces = this.bloque()
112
+ let sino = null
113
+ if (this.actual().tipo === TIPOS.PALABRA_CLAVE && this.actual().valor === 'else') {
114
+ this.consumir(TIPOS.PALABRA_CLAVE, 'else')
115
+ if (this.actual().tipo === TIPOS.PALABRA_CLAVE && this.actual().valor === 'if') {
116
+ sino = this.declIf()
117
+ } else {
118
+ sino = { tipo: 'Bloque', cuerpo: this.bloque() }
119
+ }
120
+ }
121
+ return { tipo: 'If', condicion, entonces, sino }
122
+ }
123
+
124
+ declWhile() {
125
+ this.consumir(TIPOS.PALABRA_CLAVE, 'while')
126
+ const condicion = this.expr()
127
+ const cuerpo = this.bloque()
128
+ return { tipo: 'While', condicion, cuerpo }
129
+ }
130
+
131
+ declFor() {
132
+ this.consumir(TIPOS.PALABRA_CLAVE, 'for')
133
+ const variable = this.consumir(TIPOS.IDENTIFICADOR).valor
134
+ this.consumir(TIPOS.PALABRA_CLAVE, 'in')
135
+ const lista = this.expr()
136
+ const cuerpo = this.bloque()
137
+ return { tipo: 'For', variable, lista, cuerpo }
138
+ }
139
+
140
+ declTry() {
141
+ this.consumir(TIPOS.PALABRA_CLAVE, 'try')
142
+ const intentar = this.bloque()
143
+ this.consumir(TIPOS.PALABRA_CLAVE, 'catch')
144
+ const errorVar = this.consumir(TIPOS.IDENTIFICADOR).valor
145
+ const capturar = this.bloque()
146
+ return { tipo: 'Try', intentar, errorVar, capturar }
147
+ }
148
+
149
+ declThrow() {
150
+ this.consumir(TIPOS.PALABRA_CLAVE, 'throw')
151
+ const valor = this.expr()
152
+ this.saltarLineas()
153
+ return { tipo: 'Throw', valor }
154
+ }
155
+
156
+ declReturn() {
157
+ this.consumir(TIPOS.PALABRA_CLAVE, 'return')
158
+ const valor = this.actual().tipo === TIPOS.NUEVA_LINEA ? null : this.expr()
159
+ this.saltarLineas()
160
+ return { tipo: 'Return', valor }
161
+ }
162
+
163
+ bloque() {
164
+ this.saltarLineas()
165
+ if (this.actual().tipo !== TIPOS.INDENTAR) {
166
+ // bloque de una sola expresión en la misma línea
167
+ const s = this.stmt()
168
+ return [s]
169
+ }
170
+ this.consumir(TIPOS.INDENTAR)
171
+ const stmts = []
172
+ this.saltarLineas()
173
+ while (this.actual().tipo !== TIPOS.DESINDENTAR && this.actual().tipo !== TIPOS.FIN) {
174
+ stmts.push(this.stmt())
175
+ this.saltarLineas()
176
+ }
177
+ if (this.actual().tipo === TIPOS.DESINDENTAR) this.consumir(TIPOS.DESINDENTAR)
178
+ return stmts
179
+ }
180
+
181
+ exprStmt() {
182
+ const e = this.expr()
183
+ this.saltarLineas()
184
+ return { tipo: 'ExprStmt', expr: e }
185
+ }
186
+
187
+ expr() { return this.exprAsignacion() }
188
+
189
+ exprAsignacion() {
190
+ const izq = this.exprOr()
191
+ if (this.actual().tipo === TIPOS.IGUAL) {
192
+ this.consumir(TIPOS.IGUAL)
193
+ const der = this.expr()
194
+ return { tipo: 'Asignacion', objetivo: izq, valor: der }
195
+ }
196
+ return izq
197
+ }
198
+
199
+ exprOr() {
200
+ let izq = this.exprAnd()
201
+ while (this.actual().tipo === TIPOS.PALABRA_CLAVE && this.actual().valor === 'or') {
202
+ this.consumir(TIPOS.PALABRA_CLAVE)
203
+ izq = { tipo: 'BinOp', op: 'or', izq, der: this.exprAnd() }
204
+ }
205
+ return izq
206
+ }
207
+
208
+ exprAnd() {
209
+ let izq = this.exprIgualdad()
210
+ while (this.actual().tipo === TIPOS.PALABRA_CLAVE && this.actual().valor === 'and') {
211
+ this.consumir(TIPOS.PALABRA_CLAVE)
212
+ izq = { tipo: 'BinOp', op: 'and', izq, der: this.exprIgualdad() }
213
+ }
214
+ return izq
215
+ }
216
+
217
+ exprIgualdad() {
218
+ let izq = this.exprComparacion()
219
+ while ([TIPOS.IGUAL_IGUAL, TIPOS.NO_IGUAL].includes(this.actual().tipo)) {
220
+ const op = this.consumir(this.actual().tipo).valor
221
+ izq = { tipo: 'BinOp', op, izq, der: this.exprComparacion() }
222
+ }
223
+ return izq
224
+ }
225
+
226
+ exprComparacion() {
227
+ let izq = this.exprSuma()
228
+ while ([TIPOS.MAYOR, TIPOS.MENOR, TIPOS.MAYOR_IGUAL, TIPOS.MENOR_IGUAL].includes(this.actual().tipo)) {
229
+ const op = this.consumir(this.actual().tipo).valor
230
+ izq = { tipo: 'BinOp', op, izq, der: this.exprSuma() }
231
+ }
232
+ return izq
233
+ }
234
+
235
+ exprSuma() {
236
+ let izq = this.exprProducto()
237
+ while ([TIPOS.MAS, TIPOS.MENOS].includes(this.actual().tipo)) {
238
+ const op = this.consumir(this.actual().tipo).valor
239
+ izq = { tipo: 'BinOp', op, izq, der: this.exprProducto() }
240
+ }
241
+ return izq
242
+ }
243
+
244
+ exprProducto() {
245
+ let izq = this.exprUnario()
246
+ while ([TIPOS.POR, TIPOS.ENTRE].includes(this.actual().tipo)) {
247
+ const op = this.consumir(this.actual().tipo).valor
248
+ izq = { tipo: 'BinOp', op, izq, der: this.exprUnario() }
249
+ }
250
+ return izq
251
+ }
252
+
253
+ exprUnario() {
254
+ if (this.actual().tipo === TIPOS.MENOS) {
255
+ this.consumir(TIPOS.MENOS)
256
+ return { tipo: 'Unario', op: '-', valor: this.exprUnario() }
257
+ }
258
+ if (this.actual().tipo === TIPOS.PALABRA_CLAVE && this.actual().valor === 'not') {
259
+ this.consumir(TIPOS.PALABRA_CLAVE)
260
+ return { tipo: 'Unario', op: 'not', valor: this.exprUnario() }
261
+ }
262
+ return this.exprLlamada()
263
+ }
264
+
265
+ exprLlamada() {
266
+ let expr = this.exprPrimaria()
267
+ while (true) {
268
+ if (this.actual().tipo === TIPOS.PAREN_IZQ) {
269
+ this.consumir(TIPOS.PAREN_IZQ)
270
+ const args = []
271
+ while (this.actual().tipo !== TIPOS.PAREN_DER) {
272
+ args.push(this.expr())
273
+ if (this.actual().tipo === TIPOS.COMA) this.consumir(TIPOS.COMA)
274
+ }
275
+ this.consumir(TIPOS.PAREN_DER)
276
+ expr = { tipo: 'Llamada', fn: expr, args }
277
+ } else if (this.actual().tipo === TIPOS.PUNTO) {
278
+ this.consumir(TIPOS.PUNTO)
279
+ const prop = this.consumir(TIPOS.IDENTIFICADOR).valor
280
+ expr = { tipo: 'Acceso', objeto: expr, propiedad: prop }
281
+ } else if (this.actual().tipo === TIPOS.CORCHETE_IZQ) {
282
+ this.consumir(TIPOS.CORCHETE_IZQ)
283
+ const indice = this.expr()
284
+ this.consumir(TIPOS.CORCHETE_DER)
285
+ expr = { tipo: 'Indice', objeto: expr, indice }
286
+ } else {
287
+ break
288
+ }
289
+ }
290
+ return expr
291
+ }
292
+
293
+ exprPrimaria() {
294
+ const t = this.actual()
295
+
296
+ if (t.tipo === TIPOS.NUMERO) { this.pos++; return { tipo: 'Numero', valor: t.valor } }
297
+ if (t.tipo === TIPOS.TEXTO) { this.pos++; return { tipo: 'Texto', valor: t.valor } }
298
+ if (t.tipo === TIPOS.BOOLEANO) { this.pos++; return { tipo: 'Booleano', valor: t.valor } }
299
+ if (t.tipo === TIPOS.NULO) { this.pos++; return { tipo: 'Nulo', valor: null } }
300
+
301
+ if (t.tipo === TIPOS.IDENTIFICADOR) {
302
+ this.pos++
303
+ return { tipo: 'Identificador', nombre: t.valor }
304
+ }
305
+
306
+ if (t.tipo === TIPOS.PALABRA_CLAVE && t.valor === 'fn') {
307
+ return this.declFn()
308
+ }
309
+
310
+ if (t.tipo === TIPOS.PAREN_IZQ) {
311
+ this.consumir(TIPOS.PAREN_IZQ)
312
+ const e = this.expr()
313
+ this.consumir(TIPOS.PAREN_DER)
314
+ return e
315
+ }
316
+
317
+ if (t.tipo === TIPOS.CORCHETE_IZQ) {
318
+ this.consumir(TIPOS.CORCHETE_IZQ)
319
+ const elementos = []
320
+ while (this.actual().tipo !== TIPOS.CORCHETE_DER) {
321
+ elementos.push(this.expr())
322
+ if (this.actual().tipo === TIPOS.COMA) this.consumir(TIPOS.COMA)
323
+ }
324
+ this.consumir(TIPOS.CORCHETE_DER)
325
+ return { tipo: 'Lista', elementos }
326
+ }
327
+
328
+ if (t.tipo === TIPOS.LLAVE_IZQ) {
329
+ this.consumir(TIPOS.LLAVE_IZQ)
330
+ this.saltarEstructura()
331
+ const pares = []
332
+ while (this.actual().tipo !== TIPOS.LLAVE_DER) {
333
+ const clave = this.consumir(TIPOS.IDENTIFICADOR).valor
334
+ this.consumir(TIPOS.DOS_PUNTOS)
335
+ const valor = this.expr()
336
+ pares.push({ clave, valor })
337
+ this.saltarEstructura()
338
+ if (this.actual().tipo === TIPOS.COMA) { this.consumir(TIPOS.COMA); this.saltarEstructura() }
339
+ }
340
+ this.consumir(TIPOS.LLAVE_DER)
341
+ return { tipo: 'Objeto', pares }
342
+ }
343
+
344
+ throw new Error(`[NexicScript] Token inesperado '${t.valor}' (${t.tipo}) en línea ${t.linea}`)
345
+ }
346
+ }
347
+
348
+ function parsear(tokens) {
349
+ return new Parser(tokens).parsear()
350
+ }
351
+
352
+ module.exports = { parsear }