xanascript 2.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/README.md +132 -0
- package/VERSION +1 -0
- package/llms.txt +322 -0
- package/package.json +33 -0
- package/src/ast.js +65 -0
- package/src/bench.js +68 -0
- package/src/bytecode/compiler.js +189 -0
- package/src/bytecode/opcodes.js +36 -0
- package/src/bytecode/vm.js +216 -0
- package/src/cli.js +535 -0
- package/src/codegen.js +101 -0
- package/src/codegen_opt.js +352 -0
- package/src/codegen_wasm.js +306 -0
- package/src/docsgen.js +262 -0
- package/src/errors.js +162 -0
- package/src/interpreter.js +471 -0
- package/src/lexer.js +195 -0
- package/src/lsp.js +304 -0
- package/src/macros.js +132 -0
- package/src/optimizer.js +175 -0
- package/src/orm.js +120 -0
- package/src/parser.js +819 -0
- package/src/pkgmgr.js +273 -0
- package/src/runtime.js +171 -0
- package/src/sourcemap.js +120 -0
- package/src/testrunner.js +118 -0
- package/src/wasm_binary.js +573 -0
- package/std/math.xs +60 -0
package/src/pkgmgr.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
|
|
5
|
+
const XS_PACKAGE_REGISTRY = "https://api.xanascript.dev/packages";
|
|
6
|
+
const XS_CACHE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || ".xs", ".xs", "packages");
|
|
7
|
+
const XS_PACKAGE_FILE = "xspack.json";
|
|
8
|
+
|
|
9
|
+
export async function initProject(dir) {
|
|
10
|
+
dir = dir || ".";
|
|
11
|
+
const target = path.resolve(dir);
|
|
12
|
+
if (!fs.existsSync(target)) fs.mkdirSync(target, { recursive: true });
|
|
13
|
+
|
|
14
|
+
const pkgFile = path.join(target, XS_PACKAGE_FILE);
|
|
15
|
+
if (fs.existsSync(pkgFile)) {
|
|
16
|
+
console.log(" xspack.json já existe");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const name = path.basename(target);
|
|
21
|
+
const pkg = {
|
|
22
|
+
name: name.toLowerCase().replace(/\s+/g, "-"),
|
|
23
|
+
version: "1.0.0",
|
|
24
|
+
description: "",
|
|
25
|
+
main: "src/index.xs",
|
|
26
|
+
dependencies: {},
|
|
27
|
+
xs_version: ">=2.0.0",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
|
|
31
|
+
|
|
32
|
+
const srcDir = path.join(target, "src");
|
|
33
|
+
if (!fs.existsSync(srcDir)) fs.mkdirSync(srcDir, { recursive: true });
|
|
34
|
+
|
|
35
|
+
const indexFile = path.join(srcDir, "index.xs");
|
|
36
|
+
if (!fs.existsSync(indexFile)) {
|
|
37
|
+
fs.writeFileSync(indexFile, `PARTIU()
|
|
38
|
+
SOLTA O GRITO("Olá do pacote ${pkg.name}!")
|
|
39
|
+
ACABOU()
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const gitignore = path.join(target, ".gitignore");
|
|
44
|
+
if (!fs.existsSync(gitignore)) {
|
|
45
|
+
fs.writeFileSync(gitignore, "node_modules/\n.xs-cache/\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(` Projeto XanaScript criado em ${target}`);
|
|
49
|
+
console.log(` ${XS_PACKAGE_FILE}`);
|
|
50
|
+
console.log(` src/index.xs`);
|
|
51
|
+
console.log("");
|
|
52
|
+
console.log(" Para instalar dependências:");
|
|
53
|
+
console.log(" xs install");
|
|
54
|
+
console.log(" Para publicar:");
|
|
55
|
+
console.log(" xs publish");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function installPackages(packages) {
|
|
59
|
+
if (!fs.existsSync(XS_CACHE_DIR)) {
|
|
60
|
+
fs.mkdirSync(XS_CACHE_DIR, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (packages.length === 0) {
|
|
64
|
+
|
|
65
|
+
const pkgFile = findPackageFile();
|
|
66
|
+
if (!pkgFile) {
|
|
67
|
+
console.log(" Nenhum xspack.json encontrado");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf-8"));
|
|
71
|
+
packages = Object.keys(pkg.dependencies || {});
|
|
72
|
+
if (packages.length === 0) {
|
|
73
|
+
console.log(" Nenhuma dependência para instalar");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const pkgName of packages) {
|
|
79
|
+
const [name, version] = pkgName.includes("@")
|
|
80
|
+
? pkgName.split("@")
|
|
81
|
+
: [pkgName, "latest"];
|
|
82
|
+
|
|
83
|
+
console.log(` Instalando ${name}...`);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
|
|
87
|
+
const installed = await installFromRegistry(name, version);
|
|
88
|
+
|
|
89
|
+
if (installed) {
|
|
90
|
+
|
|
91
|
+
const pkgFile = findPackageFile();
|
|
92
|
+
if (pkgFile) {
|
|
93
|
+
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf-8"));
|
|
94
|
+
if (!pkg.dependencies) pkg.dependencies = {};
|
|
95
|
+
pkg.dependencies[name] = version;
|
|
96
|
+
fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
|
|
97
|
+
}
|
|
98
|
+
console.log(` ${name}@${version}`);
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.error(` ${name}: ${e.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function installFromRegistry(name, version) {
|
|
107
|
+
|
|
108
|
+
const searchUrl = `https://api.github.com/search/repositories?q=${name}+language:xs`;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch(searchUrl, {
|
|
112
|
+
headers: { "Accept": "application/vnd.github.v3+json" }
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!res.ok) throw new Error(`GitHub API: ${res.status}`);
|
|
116
|
+
|
|
117
|
+
const data = await res.json();
|
|
118
|
+
const repo = data.items?.find(r =>
|
|
119
|
+
r.name.toLowerCase() === name.toLowerCase() ||
|
|
120
|
+
r.full_name.toLowerCase() === name.toLowerCase()
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (!repo) {
|
|
124
|
+
|
|
125
|
+
return installFromNpm(name, version);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const distDir = path.join(XS_CACHE_DIR, name);
|
|
129
|
+
if (!fs.existsSync(distDir)) fs.mkdirSync(distDir, { recursive: true });
|
|
130
|
+
|
|
131
|
+
const pkgUrl = `https://raw.githubusercontent.com/${repo.full_name}/main/xspack.json`;
|
|
132
|
+
const pkgRes = await fetch(pkgUrl);
|
|
133
|
+
|
|
134
|
+
if (pkgRes.ok) {
|
|
135
|
+
const pkgData = await pkgRes.json();
|
|
136
|
+
fs.writeFileSync(path.join(distDir, XS_PACKAGE_FILE), JSON.stringify(pkgData, null, 2));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const srcUrl = `https://raw.githubusercontent.com/${repo.full_name}/main/src/index.xs`;
|
|
140
|
+
const srcRes = await fetch(srcUrl);
|
|
141
|
+
|
|
142
|
+
if (srcRes.ok) {
|
|
143
|
+
const code = await srcRes.text();
|
|
144
|
+
const srcDir = path.join(distDir, "src");
|
|
145
|
+
if (!fs.existsSync(srcDir)) fs.mkdirSync(srcDir, { recursive: true });
|
|
146
|
+
fs.writeFileSync(path.join(srcDir, "index.xs"), code);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return true;
|
|
150
|
+
} catch (e) {
|
|
151
|
+
|
|
152
|
+
return installFromNpm(name, version);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function installFromNpm(name, version) {
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const npmUrl = `https://registry.npmjs.org/xanascript-${name}/latest`;
|
|
160
|
+
const res = await fetch(npmUrl);
|
|
161
|
+
|
|
162
|
+
if (!res.ok) throw new Error(`Pacote ${name} não encontrado`);
|
|
163
|
+
|
|
164
|
+
const data = await res.json();
|
|
165
|
+
const ver = version === "latest" ? data["dist-tags"]?.latest : version;
|
|
166
|
+
const pkgVersion = data.versions?.[ver];
|
|
167
|
+
|
|
168
|
+
if (!pkgVersion) throw new Error(`Versão ${ver} não encontrada`);
|
|
169
|
+
|
|
170
|
+
const distDir = path.join(XS_CACHE_DIR, name);
|
|
171
|
+
if (!fs.existsSync(distDir)) fs.mkdirSync(distDir, { recursive: true });
|
|
172
|
+
|
|
173
|
+
fs.writeFileSync(path.join(distDir, XS_PACKAGE_FILE), JSON.stringify({
|
|
174
|
+
name, version: ver,
|
|
175
|
+
description: pkgVersion.description || "",
|
|
176
|
+
main: "src/index.xs",
|
|
177
|
+
}, null, 2));
|
|
178
|
+
|
|
179
|
+
if (pkgVersion.dist?.tarball) {
|
|
180
|
+
const tarballRes = await fetch(pkgVersion.dist.tarball);
|
|
181
|
+
|
|
182
|
+
console.log(` npm package: ${pkgVersion.dist.tarball}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return true;
|
|
186
|
+
} catch (e) {
|
|
187
|
+
throw new Error(`Pacote não encontrado em nenhum registro: ${name}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function publishPackage() {
|
|
192
|
+
const pkgFile = findPackageFile();
|
|
193
|
+
if (!pkgFile) {
|
|
194
|
+
console.log(" xspack.json não encontrado");
|
|
195
|
+
console.log(" Crie um com: xs init");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf-8"));
|
|
200
|
+
console.log(` Publicando ${pkg.name} v${pkg.version}...`);
|
|
201
|
+
console.log("");
|
|
202
|
+
console.log(" Para publicar seu pacote XanaScript:");
|
|
203
|
+
console.log(" 1. Crie um repositório no GitHub");
|
|
204
|
+
console.log(" 2. Adicione o tópico 'xanascript-package'");
|
|
205
|
+
console.log(" 3. Commit seu código na branch main");
|
|
206
|
+
console.log("");
|
|
207
|
+
console.log(" Estrutura esperada:");
|
|
208
|
+
console.log(" xspack.json");
|
|
209
|
+
console.log(" src/index.xs ← entry point");
|
|
210
|
+
console.log(" README.xs.md ← documentação");
|
|
211
|
+
console.log("");
|
|
212
|
+
console.log(" Exemplo de xspack.json:");
|
|
213
|
+
console.log(" {");
|
|
214
|
+
console.log(` "name": "${pkg.name}",`);
|
|
215
|
+
console.log(` "version": "${pkg.version}",`);
|
|
216
|
+
console.log(' "description": "...",');
|
|
217
|
+
console.log(' "main": "src/index.xs",');
|
|
218
|
+
console.log(' "dependencies": {}');
|
|
219
|
+
console.log(" }");
|
|
220
|
+
console.log("");
|
|
221
|
+
console.log(" Após publicar no GitHub, outros devs podem instalar com:");
|
|
222
|
+
console.log(` xs install ${pkg.name}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function searchPackages(query) {
|
|
226
|
+
console.log(` Buscando pacotes XanaScript...`);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const searchUrl = `https://api.github.com/search/repositories?q=${name}+language:xs`;
|
|
230
|
+
|
|
231
|
+
const res = await fetch(searchUrl, {
|
|
232
|
+
headers: { "Accept": "application/vnd.github.v3+json" }
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (!res.ok) throw new Error(`GitHub API: ${res.status}`);
|
|
236
|
+
|
|
237
|
+
const data = await res.json();
|
|
238
|
+
|
|
239
|
+
if (data.items?.length > 0) {
|
|
240
|
+
console.log(`\n ${data.items.length} pacote(s) encontrado(s):\n`);
|
|
241
|
+
for (const repo of data.items.slice(0, 20)) {
|
|
242
|
+
console.log(` ${repo.full_name}`);
|
|
243
|
+
console.log(` ⭐ ${repo.stargazers_count} | ${repo.description || "Sem descrição"}`);
|
|
244
|
+
console.log(` ${repo.html_url}`);
|
|
245
|
+
console.log("");
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
console.log(" Nenhum pacote encontrado.");
|
|
249
|
+
console.log(" Dica: crie um repositório GitHub e adicione");
|
|
250
|
+
console.log(" o tópico 'xanascript-package'");
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
console.error(` Erro na busca: ${e.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function findPackageFile() {
|
|
258
|
+
let dir = process.cwd();
|
|
259
|
+
while (dir !== path.dirname(dir)) {
|
|
260
|
+
const pkgFile = path.join(dir, XS_PACKAGE_FILE);
|
|
261
|
+
if (fs.existsSync(pkgFile)) return pkgFile;
|
|
262
|
+
dir = path.dirname(dir);
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function getInstalledPackages() {
|
|
268
|
+
if (!fs.existsSync(XS_CACHE_DIR)) return [];
|
|
269
|
+
return fs.readdirSync(XS_CACHE_DIR).filter(d => {
|
|
270
|
+
const pkgFile = path.join(XS_CACHE_DIR, d, XS_PACKAGE_FILE);
|
|
271
|
+
return fs.existsSync(pkgFile);
|
|
272
|
+
});
|
|
273
|
+
}
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import http from "http";
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
|
|
6
|
+
import { lex } from "./lexer.js";
|
|
7
|
+
import { parse } from "./parser.js";
|
|
8
|
+
import { optimize } from "./optimizer.js";
|
|
9
|
+
import { interpret, setTabelas } from "./interpreter.js";
|
|
10
|
+
import { setSource, XSError, formatError } from "./errors.js";
|
|
11
|
+
import { criarRepositorio } from "./orm.js";
|
|
12
|
+
|
|
13
|
+
const CACHE = new Map();
|
|
14
|
+
const LOADING = new Set();
|
|
15
|
+
|
|
16
|
+
export async function runXS(code, baseDir = process.cwd(), fileName = "input.xs") {
|
|
17
|
+
setSource(code, fileName);
|
|
18
|
+
|
|
19
|
+
const tokens = lex(code, fileName);
|
|
20
|
+
|
|
21
|
+
let ast = parse(tokens);
|
|
22
|
+
ast = optimize(ast);
|
|
23
|
+
|
|
24
|
+
const env = createEnv(baseDir);
|
|
25
|
+
return interpret(ast, env);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createEnv(baseDir) {
|
|
29
|
+
const servers = new Set();
|
|
30
|
+
|
|
31
|
+
const builtins = {
|
|
32
|
+
|
|
33
|
+
SOLTA_O_GRITO: (...a) => console.log(...a),
|
|
34
|
+
FALA_BAIXO: (...a) => console.warn(...a),
|
|
35
|
+
|
|
36
|
+
AGORA: () => Date.now(),
|
|
37
|
+
|
|
38
|
+
AGORA_VAI: async url => {
|
|
39
|
+
try {
|
|
40
|
+
const res = await axios.get(url, { timeout: 3000 });
|
|
41
|
+
return res.data;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
throw new XSError(`Falha em AGORA_VAI("${url}"): ${e.message}`, {
|
|
44
|
+
hint: "Verifique se a URL está correta e acessível",
|
|
45
|
+
code: "E100",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
ESPERA_AI: ms => new Promise(r => setTimeout(r, ms)),
|
|
51
|
+
SORTEIA: (a, b) => Math.floor(Math.random() * (b - a + 1)) + a,
|
|
52
|
+
PARSEIA: JSON.parse,
|
|
53
|
+
OUVE_AQUI: k => process.env[k] ?? null,
|
|
54
|
+
|
|
55
|
+
CRIA_SERVIDOR: (port, handler) => {
|
|
56
|
+
const server = http.createServer(async (req, res) => {
|
|
57
|
+
const resposta = {
|
|
58
|
+
enviar: (dados, tipo) => {
|
|
59
|
+
if (tipo) res.setHeader("Content-Type", tipo);
|
|
60
|
+
res.end(String(dados));
|
|
61
|
+
},
|
|
62
|
+
json: (dados) => {
|
|
63
|
+
res.setHeader("Content-Type", "application/json");
|
|
64
|
+
res.end(JSON.stringify(dados));
|
|
65
|
+
},
|
|
66
|
+
status: (codigo) => {
|
|
67
|
+
res.statusCode = codigo;
|
|
68
|
+
return resposta;
|
|
69
|
+
},
|
|
70
|
+
cabecalho: (chave, valor) => {
|
|
71
|
+
res.setHeader(chave, valor);
|
|
72
|
+
return resposta;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const requisicao = {
|
|
77
|
+
url: req.url,
|
|
78
|
+
metodo: req.method,
|
|
79
|
+
cabecalhos: req.headers,
|
|
80
|
+
corpo: await new Promise(resolve => {
|
|
81
|
+
let body = "";
|
|
82
|
+
req.on("data", c => body += c);
|
|
83
|
+
req.on("end", () => resolve(body));
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await handler(requisicao, resposta);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
res.statusCode = 500;
|
|
91
|
+
res.end("Erro interno: " + e.message);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
server.listen(port, () => {
|
|
96
|
+
console.log(` Servidor rodando em http://localhost:${port}`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
servers.add(server);
|
|
100
|
+
return server;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
PARA_SERVIDOR: (server) => {
|
|
104
|
+
server.close();
|
|
105
|
+
servers.delete(server);
|
|
106
|
+
console.log(" Servidor parado");
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
CRIA_REPOSITORIO: (nomeTabela) => {
|
|
110
|
+
const t = TABELAS[nomeTabela];
|
|
111
|
+
const props = t?.props || [];
|
|
112
|
+
return criarRepositorio(nomeTabela, props);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
__IMPORT__: async mod => {
|
|
116
|
+
let full;
|
|
117
|
+
if (mod.startsWith(".") || mod.startsWith("/")) {
|
|
118
|
+
full = path.resolve(baseDir, mod);
|
|
119
|
+
} else {
|
|
120
|
+
full = mod;
|
|
121
|
+
return await importNodeModule(mod);
|
|
122
|
+
}
|
|
123
|
+
if (CACHE.has(full)) {
|
|
124
|
+
return CACHE.get(full);
|
|
125
|
+
}
|
|
126
|
+
if (LOADING.has(full)) {
|
|
127
|
+
throw new XSError(`Import cíclico detectado: ${mod}`, {
|
|
128
|
+
hint: "Dois arquivos se importam mutuamente",
|
|
129
|
+
code: "E101",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
LOADING.add(full);
|
|
133
|
+
const code = fs.readFileSync(full, "utf-8");
|
|
134
|
+
const exports = {};
|
|
135
|
+
const env2 = createEnv(path.dirname(full));
|
|
136
|
+
env2.EXPORTA = (name, value) => { exports[name] = value; };
|
|
137
|
+
await runModule(code, env2);
|
|
138
|
+
CACHE.set(full, exports);
|
|
139
|
+
LOADING.delete(full);
|
|
140
|
+
return exports;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
return builtins;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function importNodeModule(name) {
|
|
147
|
+
try {
|
|
148
|
+
const mod = await import(name);
|
|
149
|
+
return mod.default || mod;
|
|
150
|
+
} catch (e) {
|
|
151
|
+
throw new XSError(`Falha ao importar módulo "${name}": ${e.message}`, {
|
|
152
|
+
hint: "Verifique se o pacote está instalado (npm install)",
|
|
153
|
+
code: "E102",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function runModule(code, env) {
|
|
159
|
+
const tokens = lex(code);
|
|
160
|
+
|
|
161
|
+
let ast = parse(tokens);
|
|
162
|
+
ast = optimize(ast);
|
|
163
|
+
|
|
164
|
+
for (const stmt of ast.body) {
|
|
165
|
+
if (stmt.type === "ExportStmt") {
|
|
166
|
+
env.EXPORTA(stmt.name, env[stmt.name]);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
await interpret(stmt, env);
|
|
170
|
+
}
|
|
171
|
+
}
|
package/src/sourcemap.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export class SourceMap {
|
|
2
|
+
constructor(sourceFile, xsSource) {
|
|
3
|
+
this.file = sourceFile || "input.xs";
|
|
4
|
+
this.xsLines = xsSource?.split("\n") || [];
|
|
5
|
+
this.mappings = [];
|
|
6
|
+
this.generated = "";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
addXsLine(xsLine, jsCode) {
|
|
10
|
+
const currentJsLine = (this.generated.match(/\n/g) || []).length + 1;
|
|
11
|
+
this.mappings.push({ jsLine: currentJsLine, xsLine });
|
|
12
|
+
this.generated += jsCode;
|
|
13
|
+
return jsCode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
add(jsCode) {
|
|
17
|
+
this.generated += jsCode;
|
|
18
|
+
return jsCode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
translateError(error) {
|
|
22
|
+
if (!error || !error.stack) return error;
|
|
23
|
+
const stack = error.stack;
|
|
24
|
+
const lines = stack.split("\n");
|
|
25
|
+
|
|
26
|
+
const translated = lines.map(line => {
|
|
27
|
+
|
|
28
|
+
const match = line.match(/:(\d+):\d+/);
|
|
29
|
+
if (!match) return line;
|
|
30
|
+
const jsLine = parseInt(match[1]);
|
|
31
|
+
|
|
32
|
+
let bestXsLine = null;
|
|
33
|
+
for (const m of this.mappings) {
|
|
34
|
+
if (m.jsLine <= jsLine) {
|
|
35
|
+
bestXsLine = m.xsLine;
|
|
36
|
+
} else break;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (bestXsLine !== null) {
|
|
40
|
+
const xsContent = this.xsLines[bestXsLine - 1]?.trim() || "";
|
|
41
|
+
return line.replace(/:(\d+):(\d+)/, `:${bestXsLine}:1`) +
|
|
42
|
+
` ← xs:${bestXsLine} ${xsContent}`;
|
|
43
|
+
}
|
|
44
|
+
return line;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
error.stack = translated.join("\n");
|
|
48
|
+
error.xsLine = this._findXsLine(error);
|
|
49
|
+
return error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_findXsLine(error) {
|
|
53
|
+
const match = error.stack?.match(/xs:(\d+)/);
|
|
54
|
+
return match ? parseInt(match[1]) : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
toComment() {
|
|
58
|
+
const data = {
|
|
59
|
+
file: this.file,
|
|
60
|
+
version: 1,
|
|
61
|
+
mappings: this.mappings,
|
|
62
|
+
};
|
|
63
|
+
return `\n//# sourceMappingURL=data:application/json;base64,${btoa(JSON.stringify(data))}\n`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static fromComment(comment, xsLines) {
|
|
67
|
+
const match = comment.match(/sourceMap=(\{.+?\})/);
|
|
68
|
+
if (!match) return null;
|
|
69
|
+
try {
|
|
70
|
+
const data = JSON.parse(match[1]);
|
|
71
|
+
const sm = new SourceMap(data.file, xsLines?.join("\n"));
|
|
72
|
+
sm.mappings = data.mappings;
|
|
73
|
+
return sm;
|
|
74
|
+
} catch { return null; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static getRuntimeWrapper() {
|
|
78
|
+
return `
|
|
79
|
+
|
|
80
|
+
const __xs_handler = {
|
|
81
|
+
wrap(fn, sourceMap) {
|
|
82
|
+
if (!sourceMap) return fn;
|
|
83
|
+
return function(...args) {
|
|
84
|
+
try {
|
|
85
|
+
return fn.apply(this, args);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
const lines = (e.stack || "").split("\\\\n");
|
|
88
|
+
e.stack = lines.map(l => {
|
|
89
|
+
const m = l.match(/:(\\\\d+):\\\\d+/);
|
|
90
|
+
if (!m) return l;
|
|
91
|
+
const jsLine = parseInt(m[1]);
|
|
92
|
+
let best = null;
|
|
93
|
+
sourceMap.mappings.some(m => { if (m.jsLine <= jsLine) { best = m.xsLine; return false; } return true; });
|
|
94
|
+
if (best) l = l.replace(/:(\\\\d+):\\\\d+/, ":" + best + ":1") + " ← xs:" + best;
|
|
95
|
+
return l;
|
|
96
|
+
}).join("\\\\n");
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
};`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function generateWithSourceMap(ast, codegenFn, sourceFile, xsCode) {
|
|
106
|
+
const sm = new SourceMap(sourceFile, xsCode);
|
|
107
|
+
const code = codegenFn(ast);
|
|
108
|
+
return { code, sourceMap: sm };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function buildSourceMap(xsLines, statements) {
|
|
112
|
+
const sm = new SourceMap("input.xs", xsLines.join("\n"));
|
|
113
|
+
let jsLine = 1;
|
|
114
|
+
for (const stmt of statements) {
|
|
115
|
+
const xsLine = stmt.loc?.start?.line || stmt.xsLine || 1;
|
|
116
|
+
sm.mappings.push({ jsLine, xsLine });
|
|
117
|
+
jsLine += (stmt.generatedJs?.match(/\n/g) || []).length + 1;
|
|
118
|
+
}
|
|
119
|
+
return sm;
|
|
120
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { lex } from "./lexer.js";
|
|
4
|
+
import { parse } from "./parser.js";
|
|
5
|
+
import { optimize } from "./optimizer.js";
|
|
6
|
+
import { interpret, AssertionError } from "./interpreter.js";
|
|
7
|
+
import { createEnv } from "./runtime.js";
|
|
8
|
+
import { setSource, XSError } from "./errors.js";
|
|
9
|
+
|
|
10
|
+
export async function runTests(dir = ".") {
|
|
11
|
+
const start = Date.now();
|
|
12
|
+
const root = path.resolve(dir);
|
|
13
|
+
|
|
14
|
+
console.log("");
|
|
15
|
+
console.log("╔══════════════════════════════════════╗");
|
|
16
|
+
console.log("║ XanaScript Test Runner ║");
|
|
17
|
+
console.log("╚══════════════════════════════════════╝");
|
|
18
|
+
console.log("");
|
|
19
|
+
|
|
20
|
+
const testFiles = findTestFiles(root);
|
|
21
|
+
|
|
22
|
+
if (testFiles.length === 0) {
|
|
23
|
+
console.log(" Nenhum arquivo de teste encontrado (*test*.xs)");
|
|
24
|
+
console.log("");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log(` ${testFiles.length} arquivo(s) encontrado(s)\n`);
|
|
29
|
+
|
|
30
|
+
let totalPassed = 0;
|
|
31
|
+
let totalFailed = 0;
|
|
32
|
+
const failures = [];
|
|
33
|
+
|
|
34
|
+
for (const file of testFiles) {
|
|
35
|
+
const relPath = path.relative(root, file);
|
|
36
|
+
console.log(` ${relPath}`);
|
|
37
|
+
|
|
38
|
+
const code = fs.readFileSync(file, "utf-8");
|
|
39
|
+
const results = await runTestFile(code, file);
|
|
40
|
+
|
|
41
|
+
for (const r of results) {
|
|
42
|
+
if (r.passed) {
|
|
43
|
+
console.log(` ${r.name}`);
|
|
44
|
+
totalPassed++;
|
|
45
|
+
} else {
|
|
46
|
+
console.log(` ${r.name}`);
|
|
47
|
+
console.log(` ${r.error}`);
|
|
48
|
+
totalFailed++;
|
|
49
|
+
failures.push({ file: relPath, name: r.name, error: r.error });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (results.length === 0) {
|
|
54
|
+
console.log(` Nenhum TESTE encontrado`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log("");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
|
|
61
|
+
console.log("──────────────────────────────────────");
|
|
62
|
+
console.log(` ${totalPassed} passaram ${totalFailed} falharam`);
|
|
63
|
+
console.log(` ${elapsed}s`);
|
|
64
|
+
console.log("");
|
|
65
|
+
|
|
66
|
+
if (failures.length > 0) {
|
|
67
|
+
console.log(" Falhas:");
|
|
68
|
+
for (const f of failures) {
|
|
69
|
+
console.log(` ${f.file} > ${f.name}: ${f.error}`);
|
|
70
|
+
}
|
|
71
|
+
console.log("");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function runTestFile(code, filePath) {
|
|
77
|
+
const results = [];
|
|
78
|
+
const env = createEnv(path.dirname(filePath));
|
|
79
|
+
env.__testResults = results;
|
|
80
|
+
|
|
81
|
+
env.AFIRMA = (cond) => {
|
|
82
|
+
if (!cond) throw new AssertionError("AFIRMA recebeu falso");
|
|
83
|
+
};
|
|
84
|
+
env.ASSUNTO = (a, b) => {
|
|
85
|
+
if (a != b) throw new AssertionError(`ASSUNTO falhou: ${JSON.stringify(a)} != ${JSON.stringify(b)}`);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
setSource(code, filePath);
|
|
90
|
+
const tokens = lex(code, filePath);
|
|
91
|
+
let ast = parse(tokens);
|
|
92
|
+
ast = optimize(ast);
|
|
93
|
+
await interpret(ast, env);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
if (!(e instanceof AssertionError)) {
|
|
96
|
+
results.push({ name: "Erro no arquivo", passed: false, error: e.message });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function findTestFiles(dir) {
|
|
104
|
+
const files = [];
|
|
105
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
106
|
+
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const fullPath = path.join(dir, entry.name);
|
|
109
|
+
|
|
110
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
111
|
+
files.push(...findTestFiles(fullPath));
|
|
112
|
+
} else if (entry.isFile() && entry.name.endsWith(".xs") && entry.name.toLowerCase().includes("test")) {
|
|
113
|
+
files.push(fullPath);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return files;
|
|
118
|
+
}
|