relaxnative 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +592 -0
- package/dist/chunk-22W76CYR.js +607 -0
- package/dist/chunk-24NXCU65.js +254 -0
- package/dist/chunk-2APMRURB.js +65 -0
- package/dist/chunk-2CHBHJPT.js +607 -0
- package/dist/chunk-2I4JHZI7.js +287 -0
- package/dist/chunk-2JOHYYQO.js +607 -0
- package/dist/chunk-3GW77EWF.js +505 -0
- package/dist/chunk-5J5CAKCD.js +266 -0
- package/dist/chunk-5NTDZ7YZ.js +377 -0
- package/dist/chunk-5TA6MROS.js +529 -0
- package/dist/chunk-5WVEBKMJ.js +1019 -0
- package/dist/chunk-6O5TIEEI.js +545 -0
- package/dist/chunk-6XU5DETO.js +896 -0
- package/dist/chunk-7BIZ6P3B.js +176 -0
- package/dist/chunk-7DKO777J.js +285 -0
- package/dist/chunk-7JYWUH4Y.js +268 -0
- package/dist/chunk-7NMCEP2V.js +756 -0
- package/dist/chunk-A7N4YBP2.js +379 -0
- package/dist/chunk-AZTCDV6R.js +572 -0
- package/dist/chunk-B34XEGM6.js +559 -0
- package/dist/chunk-BFHBLVXW.js +607 -0
- package/dist/chunk-BLOJ33LO.js +65 -0
- package/dist/chunk-BYPXCWTI.js +375 -0
- package/dist/chunk-C4KJD2AN.js +1044 -0
- package/dist/chunk-CJALJTRQ.js +814 -0
- package/dist/chunk-D4PK367Z.js +627 -0
- package/dist/chunk-DCWBZPEV.js +287 -0
- package/dist/chunk-DI7KSUEC.js +676 -0
- package/dist/chunk-DQ2KXIOO.js +665 -0
- package/dist/chunk-DV5STE3W.js +986 -0
- package/dist/chunk-EG5KNEKP.js +1027 -0
- package/dist/chunk-EOA2OWFA.js +1020 -0
- package/dist/chunk-ES3B6EZJ.js +56 -0
- package/dist/chunk-ETIXNPU5.js +741 -0
- package/dist/chunk-EUZBU2H7.js +824 -0
- package/dist/chunk-F6V7XDEB.js +150 -0
- package/dist/chunk-FNJKUFNF.js +1019 -0
- package/dist/chunk-FZB37DWL.js +453 -0
- package/dist/chunk-G3NDHZNZ.js +453 -0
- package/dist/chunk-G4YR34LE.js +410 -0
- package/dist/chunk-GU4XXISM.js +264 -0
- package/dist/chunk-GVPSQXGJ.js +1027 -0
- package/dist/chunk-HD5C4RNU.js +676 -0
- package/dist/chunk-HDIVY47T.js +287 -0
- package/dist/chunk-HFLRTDNK.js +985 -0
- package/dist/chunk-HGWRCVQ5.js +287 -0
- package/dist/chunk-HRG3SVKK.js +995 -0
- package/dist/chunk-HUGFULJ3.js +1027 -0
- package/dist/chunk-IDYSBXYS.js +344 -0
- package/dist/chunk-ISDDUQVI.js +1019 -0
- package/dist/chunk-IZ632ZCJ.js +286 -0
- package/dist/chunk-J5XI4L52.js +218 -0
- package/dist/chunk-JTIO7BUH.js +582 -0
- package/dist/chunk-JTWSFMF2.js +1020 -0
- package/dist/chunk-K5TV62T4.js +736 -0
- package/dist/chunk-K7MTG53V.js +985 -0
- package/dist/chunk-KGLZB3H2.js +676 -0
- package/dist/chunk-KYAB35P5.js +741 -0
- package/dist/chunk-KYDW3YVX.js +453 -0
- package/dist/chunk-L3MEMPRH.js +361 -0
- package/dist/chunk-LFTO3Z7N.js +757 -0
- package/dist/chunk-LLZ4I4OR.js +405 -0
- package/dist/chunk-LMRUM4U4.js +207 -0
- package/dist/chunk-LT5OGU6T.js +559 -0
- package/dist/chunk-LZQQOC3M.js +741 -0
- package/dist/chunk-LZYUNF6Q.js +1017 -0
- package/dist/chunk-MCTPVW4G.js +453 -0
- package/dist/chunk-MGLWXBIB.js +65 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-MTE6XDGC.js +541 -0
- package/dist/chunk-NDJUNDAE.js +676 -0
- package/dist/chunk-NG7SNFUD.js +1027 -0
- package/dist/chunk-ONVWKYK7.js +739 -0
- package/dist/chunk-OVMTFGE7.js +1042 -0
- package/dist/chunk-P5RQPRJ4.js +741 -0
- package/dist/chunk-PFABSW6Y.js +401 -0
- package/dist/chunk-PVVUJA2M.js +65 -0
- package/dist/chunk-Q3CDTGTX.js +676 -0
- package/dist/chunk-QKAKWDOQ.js +967 -0
- package/dist/chunk-QMPRDU6I.js +598 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RB6RHB6C.js +254 -0
- package/dist/chunk-RD2K7ODW.js +175 -0
- package/dist/chunk-RHBHJND2.js +582 -0
- package/dist/chunk-RUP6POSE.js +453 -0
- package/dist/chunk-RYNSM23L.js +582 -0
- package/dist/chunk-S72S7XXS.js +255 -0
- package/dist/chunk-SDV5AAPW.js +213 -0
- package/dist/chunk-STXQPXUY.js +242 -0
- package/dist/chunk-TUSF5AEG.js +140 -0
- package/dist/chunk-V74SNTE6.js +736 -0
- package/dist/chunk-VHM5XETC.js +453 -0
- package/dist/chunk-VKQIXLNL.js +273 -0
- package/dist/chunk-VPG23Z7Y.js +545 -0
- package/dist/chunk-VSP234PR.js +627 -0
- package/dist/chunk-WH4JPUWF.js +598 -0
- package/dist/chunk-WLAUJL3K.js +409 -0
- package/dist/chunk-WXCN2QJ7.js +350 -0
- package/dist/chunk-X3JZKLJC.js +896 -0
- package/dist/chunk-X7SAP7FC.js +582 -0
- package/dist/chunk-XEH6PRYE.js +968 -0
- package/dist/chunk-XI65CAQV.js +211 -0
- package/dist/chunk-Y7OSHR6W.js +235 -0
- package/dist/chunk-YN4WUMVD.js +1020 -0
- package/dist/chunk-YUWJ2C4Y.js +1020 -0
- package/dist/chunk-YXLBPWNU.js +263 -0
- package/dist/chunk-YYJJHO7R.js +407 -0
- package/dist/chunk-Z2RBHUIH.js +757 -0
- package/dist/chunk-Z6G3KIOM.js +1027 -0
- package/dist/chunk-ZPPXCDSH.js +361 -0
- package/dist/chunk-ZRTY24SZ.js +582 -0
- package/dist/chunk-ZSDFBCQG.js +741 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +339 -0
- package/dist/esmLoader.d.ts +10 -0
- package/dist/esmLoader.js +112 -0
- package/dist/index.d.ts +407 -0
- package/dist/index.js +126 -0
- package/dist/memory/memory.selftest.d.ts +2 -0
- package/dist/memory/memory.selftest.js +25 -0
- package/dist/worker/processEntry.d.ts +2 -0
- package/dist/worker/processEntry.js +160 -0
- package/dist/worker/workerEntry.d.ts +2 -0
- package/dist/worker/workerEntry.js +27 -0
- package/native/examples/add.c +6 -0
- package/native/examples/add_test.c +23 -0
- package/native/relaxnative_test.h +36 -0
- package/package.json +81 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import {
|
|
2
|
+
alloc,
|
|
3
|
+
compileWithCache,
|
|
4
|
+
detectCompilers,
|
|
5
|
+
detectLanguage,
|
|
6
|
+
loadNative,
|
|
7
|
+
loadNativeWithBindings,
|
|
8
|
+
parseNativeSource
|
|
9
|
+
} from "./chunk-V74SNTE6.js";
|
|
10
|
+
import {
|
|
11
|
+
loadFfi
|
|
12
|
+
} from "./chunk-ZPPXCDSH.js";
|
|
13
|
+
|
|
14
|
+
// src/registry/registryPaths.ts
|
|
15
|
+
import { join, resolve } from "path";
|
|
16
|
+
function getProjectRoot(cwd = process.cwd()) {
|
|
17
|
+
return cwd;
|
|
18
|
+
}
|
|
19
|
+
function getRegistryRoot(projectRoot = getProjectRoot()) {
|
|
20
|
+
return join(projectRoot, "native", "registry");
|
|
21
|
+
}
|
|
22
|
+
function getInstalledPackageDir(pkg, projectRoot = getProjectRoot()) {
|
|
23
|
+
return join(getRegistryRoot(projectRoot), pkg);
|
|
24
|
+
}
|
|
25
|
+
function resolveRegistryImport(specifier, projectRoot = getProjectRoot()) {
|
|
26
|
+
const prefix = "relaxnative/";
|
|
27
|
+
if (!specifier.startsWith(prefix)) return null;
|
|
28
|
+
const pkg = specifier.slice(prefix.length);
|
|
29
|
+
return resolve(getInstalledPackageDir(pkg, projectRoot));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/registry/relaxJson.ts
|
|
33
|
+
import { readFileSync } from "fs";
|
|
34
|
+
import { join as join2 } from "path";
|
|
35
|
+
function readRelaxJson(pkgDir) {
|
|
36
|
+
const jsonPath = join2(pkgDir, "relax.json");
|
|
37
|
+
const raw = readFileSync(jsonPath, "utf8");
|
|
38
|
+
const parsed = JSON.parse(raw);
|
|
39
|
+
if (!parsed?.name || !parsed?.version || !Array.isArray(parsed?.exports)) {
|
|
40
|
+
throw new Error(`Invalid relax.json at ${jsonPath}`);
|
|
41
|
+
}
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/registry/installer.ts
|
|
46
|
+
import {
|
|
47
|
+
mkdirSync,
|
|
48
|
+
readFileSync as readFileSync3,
|
|
49
|
+
rmSync,
|
|
50
|
+
writeFileSync,
|
|
51
|
+
existsSync,
|
|
52
|
+
readdirSync
|
|
53
|
+
} from "fs";
|
|
54
|
+
import { join as join3 } from "path";
|
|
55
|
+
|
|
56
|
+
// src/registry/staticScan.ts
|
|
57
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
58
|
+
function staticScanNativeSource(sourcePath) {
|
|
59
|
+
const src = readFileSync2(sourcePath, "utf8");
|
|
60
|
+
const lines = src.split(/\r?\n/);
|
|
61
|
+
const rules = [
|
|
62
|
+
{
|
|
63
|
+
rule: "process-spawn",
|
|
64
|
+
re: /\b(system|popen|fork|execv|execve|execvp|execvpe|execl|execlp|CreateProcessW|WinExec)\b/,
|
|
65
|
+
message: "Potential process execution API found"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
rule: "raw-syscall",
|
|
69
|
+
re: /\b(syscall|__syscall)\b/,
|
|
70
|
+
message: "Raw syscall usage found"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
rule: "file-io",
|
|
74
|
+
re: /\b(fopen|open|CreateFileW|unlink|remove|rename)\b/,
|
|
75
|
+
message: "Potential filesystem API found"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
rule: "network",
|
|
79
|
+
re: /\b(socket|connect|bind|listen|accept|recv|send|getaddrinfo)\b/,
|
|
80
|
+
message: "Potential network API found"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
rule: "w-x-memory",
|
|
84
|
+
re: /\b(mprotect|VirtualProtect|PROT_EXEC|PAGE_EXECUTE_READWRITE)\b/,
|
|
85
|
+
message: "Writable/executable memory pattern found"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
rule: "dynamic-loader",
|
|
89
|
+
re: /\b(dlopen|LoadLibraryA|LoadLibraryW)\b/,
|
|
90
|
+
message: "Dynamic loader API found"
|
|
91
|
+
}
|
|
92
|
+
];
|
|
93
|
+
const findings = [];
|
|
94
|
+
for (let i = 0; i < lines.length; i++) {
|
|
95
|
+
const line = lines[i];
|
|
96
|
+
for (const r of rules) {
|
|
97
|
+
if (r.re.test(line)) {
|
|
98
|
+
findings.push({ rule: r.rule, message: r.message, line: i + 1 });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return findings;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/registry/installer.ts
|
|
106
|
+
function installPackage(pkgSpecifier, projectRoot = process.cwd()) {
|
|
107
|
+
const warnings = [];
|
|
108
|
+
const registryRoot = getRegistryRoot(projectRoot);
|
|
109
|
+
mkdirSync(registryRoot, { recursive: true });
|
|
110
|
+
const isFile = pkgSpecifier.startsWith("file:");
|
|
111
|
+
if (!isFile) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Only file: installs are supported right now (deterministic, offline). Got: ${pkgSpecifier}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const srcDir = pkgSpecifier.slice("file:".length);
|
|
117
|
+
const manifest = readRelaxJson(srcDir);
|
|
118
|
+
const destDir = getInstalledPackageDir(manifest.name, projectRoot);
|
|
119
|
+
rmSync(destDir, { recursive: true, force: true });
|
|
120
|
+
mkdirSync(destDir, { recursive: true });
|
|
121
|
+
writeFileSync(join3(destDir, "relax.json"), readFileSync3(join3(srcDir, "relax.json")));
|
|
122
|
+
for (const exp of manifest.exports) {
|
|
123
|
+
const srcPath = join3(srcDir, exp.source);
|
|
124
|
+
const dstPath = join3(destDir, exp.source);
|
|
125
|
+
mkdirSync(join3(dstPath, ".."), { recursive: true });
|
|
126
|
+
if (!existsSync(srcPath)) {
|
|
127
|
+
throw new Error(`Missing export source: ${srcPath}`);
|
|
128
|
+
}
|
|
129
|
+
const findings = staticScanNativeSource(srcPath);
|
|
130
|
+
for (const f of findings) {
|
|
131
|
+
warnings.push(`${exp.source}:${f.line ?? "?"} ${f.rule}: ${f.message}`);
|
|
132
|
+
}
|
|
133
|
+
writeFileSync(dstPath, readFileSync3(srcPath));
|
|
134
|
+
}
|
|
135
|
+
return { pkg: manifest.name, dir: destDir, warnings };
|
|
136
|
+
}
|
|
137
|
+
function removePackage(pkg, projectRoot = process.cwd()) {
|
|
138
|
+
const destDir = getInstalledPackageDir(pkg, projectRoot);
|
|
139
|
+
rmSync(destDir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
function listPackages(projectRoot = process.cwd()) {
|
|
142
|
+
const root = getRegistryRoot(projectRoot);
|
|
143
|
+
if (!existsSync(root)) return [];
|
|
144
|
+
return readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
145
|
+
}
|
|
146
|
+
function updateIndex(projectRoot = process.cwd()) {
|
|
147
|
+
const root = getRegistryRoot(projectRoot);
|
|
148
|
+
if (!existsSync(root)) return;
|
|
149
|
+
const pkgs = listPackages(projectRoot);
|
|
150
|
+
writeFileSync(join3(root, ".index"), pkgs.join("\n") + (pkgs.length ? "\n" : ""));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/nativeTestHarness.ts
|
|
154
|
+
import { readdirSync as readdirSync2, existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
|
|
155
|
+
import { extname, join as join4 } from "path";
|
|
156
|
+
var NATIVE_EXTS = /* @__PURE__ */ new Set([".c", ".cpp", ".cc", ".cxx", ".rs"]);
|
|
157
|
+
function isNativeFile(p) {
|
|
158
|
+
return NATIVE_EXTS.has(extname(p));
|
|
159
|
+
}
|
|
160
|
+
function discoverNativeTestFiles(rootDir) {
|
|
161
|
+
const out = [];
|
|
162
|
+
function walk(dir) {
|
|
163
|
+
for (const ent of readdirSync2(dir, { withFileTypes: true })) {
|
|
164
|
+
const p = join4(dir, ent.name);
|
|
165
|
+
if (ent.isDirectory()) walk(p);
|
|
166
|
+
else if (ent.isFile() && isNativeFile(p)) out.push(p);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!existsSync2(rootDir)) return [];
|
|
170
|
+
walk(rootDir);
|
|
171
|
+
out.sort((a, b) => {
|
|
172
|
+
const at = /_test\./.test(a) ? 0 : 1;
|
|
173
|
+
const bt = /_test\./.test(b) ? 0 : 1;
|
|
174
|
+
return at - bt || a.localeCompare(b);
|
|
175
|
+
});
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
function discoverNativeTestsFromFile(sourcePath) {
|
|
179
|
+
const src = readFileSync4(sourcePath, "utf8");
|
|
180
|
+
const lines = src.split(/\r?\n/);
|
|
181
|
+
const out = [];
|
|
182
|
+
const re = /^(?:\s*)(const\s+char\s*\*|int)\s+(test_[A-Za-z0-9_]+)\s*\(/;
|
|
183
|
+
for (let i = 0; i < lines.length; i++) {
|
|
184
|
+
const m = lines[i].match(re);
|
|
185
|
+
if (!m) continue;
|
|
186
|
+
const ret = m[1];
|
|
187
|
+
const name = m[2];
|
|
188
|
+
const kind = /^int\b/.test(ret) ? "int" : "cstring";
|
|
189
|
+
out.push({ name, sourcePath, line: i + 1, kind });
|
|
190
|
+
}
|
|
191
|
+
if (out.length) return out;
|
|
192
|
+
const language = detectLanguage(sourcePath);
|
|
193
|
+
const bindings = parseNativeSource(sourcePath, language);
|
|
194
|
+
const tests = [];
|
|
195
|
+
for (const [name, fn] of Object.entries(bindings.functions ?? {})) {
|
|
196
|
+
if (!name.startsWith("test_")) continue;
|
|
197
|
+
const ret = fn.returns;
|
|
198
|
+
const kind = ret === "int" || ret === "uint" || ret === "long" || ret === "size_t" ? "int" : "cstring";
|
|
199
|
+
tests.push({
|
|
200
|
+
name,
|
|
201
|
+
sourcePath,
|
|
202
|
+
line: fn.sourceLine,
|
|
203
|
+
kind
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return tests;
|
|
207
|
+
}
|
|
208
|
+
function discoverNativeTests(rootDir) {
|
|
209
|
+
const files = discoverNativeTestFiles(rootDir);
|
|
210
|
+
const all = [];
|
|
211
|
+
for (const f of files) {
|
|
212
|
+
all.push(...discoverNativeTestsFromFile(f));
|
|
213
|
+
}
|
|
214
|
+
return all;
|
|
215
|
+
}
|
|
216
|
+
function formatLocation(sourcePath, line) {
|
|
217
|
+
if (!line) return sourcePath;
|
|
218
|
+
return `${sourcePath}:${line}`;
|
|
219
|
+
}
|
|
220
|
+
function formatNativeTestResults(results) {
|
|
221
|
+
const lines = [];
|
|
222
|
+
for (const r of results) {
|
|
223
|
+
if (r.ok) {
|
|
224
|
+
lines.push(`\u2713 ${r.name}`);
|
|
225
|
+
} else {
|
|
226
|
+
const loc = formatLocation(r.sourcePath, r.line);
|
|
227
|
+
const msg = r.message ? ` (${r.message})` : "";
|
|
228
|
+
lines.push(`\u2717 ${r.name}${msg}`);
|
|
229
|
+
lines.push(` at ${loc}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const passed = results.filter((r) => r.ok).length;
|
|
233
|
+
const failed = results.length - passed;
|
|
234
|
+
lines.push("");
|
|
235
|
+
lines.push(`${passed} passed, ${failed} failed`);
|
|
236
|
+
return lines.join("\n");
|
|
237
|
+
}
|
|
238
|
+
async function runNativeTests(rootDir, opts) {
|
|
239
|
+
const isolation = opts?.isolation ?? "in-process";
|
|
240
|
+
const tests = discoverNativeTests(rootDir);
|
|
241
|
+
if (!tests.length) {
|
|
242
|
+
return { results: [], exitCode: 0 };
|
|
243
|
+
}
|
|
244
|
+
const { c, rust, platform } = detectCompilers();
|
|
245
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
246
|
+
for (const t of tests) {
|
|
247
|
+
const arr = byFile.get(t.sourcePath) ?? [];
|
|
248
|
+
arr.push(t);
|
|
249
|
+
byFile.set(t.sourcePath, arr);
|
|
250
|
+
}
|
|
251
|
+
const results = [];
|
|
252
|
+
for (const [sourcePath, cases] of byFile) {
|
|
253
|
+
const language = detectLanguage(sourcePath);
|
|
254
|
+
const compiler = language === "rust" ? rust : c;
|
|
255
|
+
if (!compiler) throw new Error(`No compiler for ${language}`);
|
|
256
|
+
const compileRes = compileWithCache(compiler, platform, {
|
|
257
|
+
sourcePath,
|
|
258
|
+
outDir: ".cache/native"
|
|
259
|
+
});
|
|
260
|
+
let api;
|
|
261
|
+
if (isolation === "in-process") {
|
|
262
|
+
const bindings = parseNativeSource(sourcePath, language);
|
|
263
|
+
api = loadFfi(compileRes.outputPath, bindings);
|
|
264
|
+
} else {
|
|
265
|
+
const synth = {
|
|
266
|
+
functions: Object.fromEntries(
|
|
267
|
+
cases.map((tc) => [
|
|
268
|
+
tc.name,
|
|
269
|
+
{
|
|
270
|
+
name: tc.name,
|
|
271
|
+
// Note: mapType supports "char*" (and "cstring" used to), but
|
|
272
|
+
// some environments/version combos reject "cstring" when sent
|
|
273
|
+
// across worker/process boundaries. Use a stable alias.
|
|
274
|
+
returns: tc.kind === "int" ? "int" : "char*",
|
|
275
|
+
args: [],
|
|
276
|
+
mode: "sync",
|
|
277
|
+
cost: "low"
|
|
278
|
+
}
|
|
279
|
+
])
|
|
280
|
+
)
|
|
281
|
+
};
|
|
282
|
+
const { mod } = await loadNativeWithBindings(sourcePath, {
|
|
283
|
+
isolation,
|
|
284
|
+
mutateBindings(bindings) {
|
|
285
|
+
bindings.functions = synth.functions;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
api = mod;
|
|
289
|
+
}
|
|
290
|
+
for (const tc of cases) {
|
|
291
|
+
const fn = api[tc.name];
|
|
292
|
+
if (typeof fn !== "function") {
|
|
293
|
+
results.push({
|
|
294
|
+
name: tc.name,
|
|
295
|
+
ok: false,
|
|
296
|
+
message: `Function not found: ${tc.name}`,
|
|
297
|
+
sourcePath: tc.sourcePath,
|
|
298
|
+
line: tc.line,
|
|
299
|
+
durationMs: 0
|
|
300
|
+
});
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const start = process.hrtime.bigint();
|
|
304
|
+
let ok = false;
|
|
305
|
+
let message;
|
|
306
|
+
try {
|
|
307
|
+
const out = fn();
|
|
308
|
+
const awaited = out instanceof Promise ? await out : out;
|
|
309
|
+
if (tc.kind === "int") {
|
|
310
|
+
ok = Number(awaited) === 0;
|
|
311
|
+
if (!ok) message = `returned ${String(awaited)}`;
|
|
312
|
+
} else {
|
|
313
|
+
ok = awaited == null || String(awaited).length === 0;
|
|
314
|
+
if (!ok) message = String(awaited);
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
ok = false;
|
|
318
|
+
message = err?.message ?? String(err);
|
|
319
|
+
}
|
|
320
|
+
const end = process.hrtime.bigint();
|
|
321
|
+
const durationMs = Number(end - start) / 1e6;
|
|
322
|
+
results.push({
|
|
323
|
+
name: tc.name,
|
|
324
|
+
ok,
|
|
325
|
+
message,
|
|
326
|
+
sourcePath: tc.sourcePath,
|
|
327
|
+
line: tc.line,
|
|
328
|
+
durationMs
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const failed = results.some((r) => !r.ok);
|
|
333
|
+
return { results, exitCode: failed ? 1 : 0 };
|
|
334
|
+
}
|
|
335
|
+
function getSourceLine(sourcePath, line) {
|
|
336
|
+
if (!line) return null;
|
|
337
|
+
try {
|
|
338
|
+
const src = readFileSync4(sourcePath, "utf8");
|
|
339
|
+
const lines = src.split(/\r?\n/);
|
|
340
|
+
return lines[line - 1] ?? null;
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/benchmark.ts
|
|
347
|
+
function nowNs() {
|
|
348
|
+
return process.hrtime.bigint();
|
|
349
|
+
}
|
|
350
|
+
function nsToMs(ns) {
|
|
351
|
+
return Number(ns) / 1e6;
|
|
352
|
+
}
|
|
353
|
+
function assertSafeRun(iterations) {
|
|
354
|
+
if (iterations > 2e6) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Refusing to run benchmark with iterations=${iterations} (cap=2,000,000). Pass a smaller value.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function benchmarkJsFunction(name, fn, opts) {
|
|
361
|
+
const iterations = opts?.iterations ?? 1e5;
|
|
362
|
+
const warmup = opts?.warmup ?? Math.min(1e4, Math.max(100, Math.floor(iterations * 0.05)));
|
|
363
|
+
assertSafeRun(iterations);
|
|
364
|
+
const args = Array.isArray(opts?.args) ? opts.args : fn.length === 0 ? [] : [1, 2];
|
|
365
|
+
for (let i = 0; i < warmup; i++) {
|
|
366
|
+
const out = fn(...args);
|
|
367
|
+
if (out instanceof Promise) await out;
|
|
368
|
+
}
|
|
369
|
+
const latencies = [];
|
|
370
|
+
const startAll = nowNs();
|
|
371
|
+
for (let i = 0; i < iterations; i++) {
|
|
372
|
+
const t0 = nowNs();
|
|
373
|
+
const out = fn(...args);
|
|
374
|
+
if (out instanceof Promise) await out;
|
|
375
|
+
const t1 = nowNs();
|
|
376
|
+
latencies.push(nsToMs(t1 - t0));
|
|
377
|
+
}
|
|
378
|
+
const endAll = nowNs();
|
|
379
|
+
const totalMs = nsToMs(endAll - startAll);
|
|
380
|
+
let min = Infinity;
|
|
381
|
+
let max = -Infinity;
|
|
382
|
+
let sum = 0;
|
|
383
|
+
for (const l of latencies) {
|
|
384
|
+
if (l < min) min = l;
|
|
385
|
+
if (l > max) max = l;
|
|
386
|
+
sum += l;
|
|
387
|
+
}
|
|
388
|
+
const avg = sum / latencies.length;
|
|
389
|
+
const callsPerSec = iterations / (totalMs / 1e3);
|
|
390
|
+
return {
|
|
391
|
+
name,
|
|
392
|
+
iterations,
|
|
393
|
+
warmup,
|
|
394
|
+
callsPerSec,
|
|
395
|
+
avgLatencyMs: avg,
|
|
396
|
+
minLatencyMs: min,
|
|
397
|
+
maxLatencyMs: max
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function getBuiltInBaseline(fnName) {
|
|
401
|
+
if (fnName === "add") {
|
|
402
|
+
return {
|
|
403
|
+
name: "traditional-js",
|
|
404
|
+
fn: (a, b) => a + b,
|
|
405
|
+
defaultArgs: [1, 2]
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (fnName === "loop_sum") {
|
|
409
|
+
return {
|
|
410
|
+
name: "traditional-js",
|
|
411
|
+
fn: (n) => {
|
|
412
|
+
let acc = 0;
|
|
413
|
+
for (let i = 0; i < n; i++) {
|
|
414
|
+
acc += (i ^ 2654435769) & 65535;
|
|
415
|
+
}
|
|
416
|
+
return acc;
|
|
417
|
+
},
|
|
418
|
+
// default to a CPU-heavy iteration count so native has a chance to win
|
|
419
|
+
defaultArgs: [5e7]
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (fnName === "sum_u8") {
|
|
423
|
+
return {
|
|
424
|
+
name: "traditional-js",
|
|
425
|
+
fn: (buf, n) => {
|
|
426
|
+
let s = 0;
|
|
427
|
+
for (let i = 0; i < n; i++) s += buf[i];
|
|
428
|
+
return s;
|
|
429
|
+
},
|
|
430
|
+
// Default to 64MB. Big enough that native often has a chance to win.
|
|
431
|
+
defaultArgs: [new Uint8Array(64 * 1024 * 1024).fill(1), 64 * 1024 * 1024]
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (fnName === "dot_f32") {
|
|
435
|
+
const n = 8e6;
|
|
436
|
+
const a = new Float32Array(n);
|
|
437
|
+
const b = new Float32Array(n);
|
|
438
|
+
for (let i = 0; i < n; i++) {
|
|
439
|
+
a[i] = i % 1024 * 1e-3;
|
|
440
|
+
b[i] = i % 2048 * 2e-3;
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
name: "traditional-js",
|
|
444
|
+
fn: (aa, bb, nn) => {
|
|
445
|
+
let acc = 0;
|
|
446
|
+
for (let i = 0; i < nn; i++) acc += aa[i] * bb[i];
|
|
447
|
+
return acc;
|
|
448
|
+
},
|
|
449
|
+
defaultArgs: [a, b, n]
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
function pickArgs(fnName, jsArity, callerArgs) {
|
|
455
|
+
if (Array.isArray(callerArgs)) return callerArgs;
|
|
456
|
+
const builtIn = getBuiltInBaseline(fnName);
|
|
457
|
+
if (builtIn) return builtIn.defaultArgs;
|
|
458
|
+
return jsArity === 0 ? [] : [1, 2];
|
|
459
|
+
}
|
|
460
|
+
function mapArgsForNative(fnName, args) {
|
|
461
|
+
if (fnName === "sum_u8") {
|
|
462
|
+
const buf = args[0];
|
|
463
|
+
const n = args[1];
|
|
464
|
+
if (buf && typeof n === "number" && ArrayBuffer.isView(buf)) {
|
|
465
|
+
const nb = alloc(buf.byteLength);
|
|
466
|
+
nb.write(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
467
|
+
return [nb, n];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (fnName === "dot_f32") {
|
|
471
|
+
const a = args[0];
|
|
472
|
+
const b = args[1];
|
|
473
|
+
const n = args[2];
|
|
474
|
+
if (a && b && typeof n === "number" && ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
|
|
475
|
+
const na = alloc(a.byteLength);
|
|
476
|
+
na.write(new Uint8Array(a.buffer, a.byteOffset, a.byteLength));
|
|
477
|
+
const nb = alloc(b.byteLength);
|
|
478
|
+
nb.write(new Uint8Array(b.buffer, b.byteOffset, b.byteLength));
|
|
479
|
+
return [na, nb, n];
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return args;
|
|
483
|
+
}
|
|
484
|
+
async function benchmarkNativeFunction(nativePath, fnName, opts) {
|
|
485
|
+
const iterations = opts?.iterations ?? 1e5;
|
|
486
|
+
const warmup = opts?.warmup ?? Math.min(1e4, Math.max(100, Math.floor(iterations * 0.05)));
|
|
487
|
+
const mode = opts?.mode ?? "sync";
|
|
488
|
+
assertSafeRun(iterations);
|
|
489
|
+
const isolation = mode === "worker" ? "worker" : "in-process";
|
|
490
|
+
const mod = await loadNative(nativePath, { isolation });
|
|
491
|
+
const fn = mod?.[fnName];
|
|
492
|
+
if (typeof fn !== "function") {
|
|
493
|
+
throw new Error(`Function not found: ${fnName}`);
|
|
494
|
+
}
|
|
495
|
+
const args = mapArgsForNative(fnName, pickArgs(fnName, fn.length, opts?.args));
|
|
496
|
+
for (let i = 0; i < warmup; i++) {
|
|
497
|
+
const out = fn(...args);
|
|
498
|
+
if (out instanceof Promise) await out;
|
|
499
|
+
}
|
|
500
|
+
const latencies = [];
|
|
501
|
+
const startAll = nowNs();
|
|
502
|
+
for (let i = 0; i < iterations; i++) {
|
|
503
|
+
const t0 = nowNs();
|
|
504
|
+
const out = fn(...args);
|
|
505
|
+
if (out instanceof Promise) await out;
|
|
506
|
+
const t1 = nowNs();
|
|
507
|
+
latencies.push(nsToMs(t1 - t0));
|
|
508
|
+
}
|
|
509
|
+
const endAll = nowNs();
|
|
510
|
+
const totalMs = nsToMs(endAll - startAll);
|
|
511
|
+
let min = Infinity;
|
|
512
|
+
let max = -Infinity;
|
|
513
|
+
let sum = 0;
|
|
514
|
+
for (const l of latencies) {
|
|
515
|
+
if (l < min) min = l;
|
|
516
|
+
if (l > max) max = l;
|
|
517
|
+
sum += l;
|
|
518
|
+
}
|
|
519
|
+
const avg = sum / latencies.length;
|
|
520
|
+
const callsPerSec = iterations / (totalMs / 1e3);
|
|
521
|
+
return {
|
|
522
|
+
fnName,
|
|
523
|
+
mode,
|
|
524
|
+
iterations,
|
|
525
|
+
warmup,
|
|
526
|
+
callsPerSec,
|
|
527
|
+
avgLatencyMs: avg,
|
|
528
|
+
minLatencyMs: min,
|
|
529
|
+
maxLatencyMs: max
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
async function benchmarkCompareSyncVsWorker(nativePath, fnName, opts) {
|
|
533
|
+
const sync = await benchmarkNativeFunction(nativePath, fnName, {
|
|
534
|
+
...opts,
|
|
535
|
+
mode: "sync"
|
|
536
|
+
});
|
|
537
|
+
const worker = await benchmarkNativeFunction(nativePath, fnName, {
|
|
538
|
+
...opts,
|
|
539
|
+
mode: "worker"
|
|
540
|
+
});
|
|
541
|
+
return { sync, worker };
|
|
542
|
+
}
|
|
543
|
+
async function benchmarkCompareTraditionalVsRelaxnative(nativePath, fnName, opts) {
|
|
544
|
+
const builtIn = getBuiltInBaseline(fnName);
|
|
545
|
+
const baseline = opts?.baseline ?? builtIn?.fn ?? (() => {
|
|
546
|
+
throw new Error(
|
|
547
|
+
`No default baseline for fn=${fnName}. Provide opts.baseline to benchmark \u201Ctraditional programming\u201D for this function.`
|
|
548
|
+
);
|
|
549
|
+
})();
|
|
550
|
+
const baselineName = opts?.baselineName ?? builtIn?.name ?? "traditional-js";
|
|
551
|
+
const traditional = await benchmarkJsFunction(baselineName, baseline, {
|
|
552
|
+
iterations: opts?.iterations,
|
|
553
|
+
warmup: opts?.warmup,
|
|
554
|
+
args: pickArgs(fnName, baseline.length, opts?.args)
|
|
555
|
+
});
|
|
556
|
+
const relaxnative = await benchmarkCompareSyncVsWorker(nativePath, fnName, opts);
|
|
557
|
+
return { traditional, relaxnative };
|
|
558
|
+
}
|
|
559
|
+
function formatBenchmarkResult(r) {
|
|
560
|
+
const f = (n) => Number.isFinite(n) ? n.toFixed(3) : String(n);
|
|
561
|
+
return [
|
|
562
|
+
`${r.fnName} (${r.mode})`,
|
|
563
|
+
` iterations: ${r.iterations} (warmup ${r.warmup})`,
|
|
564
|
+
` calls/sec: ${f(r.callsPerSec)}`,
|
|
565
|
+
` avg ms: ${f(r.avgLatencyMs)}`,
|
|
566
|
+
` min ms: ${f(r.minLatencyMs)}`,
|
|
567
|
+
` max ms: ${f(r.maxLatencyMs)}`
|
|
568
|
+
].join("\n");
|
|
569
|
+
}
|
|
570
|
+
function formatBenchmarkCompare(res) {
|
|
571
|
+
return [formatBenchmarkResult(res.sync), "", formatBenchmarkResult(res.worker)].join("\n");
|
|
572
|
+
}
|
|
573
|
+
function formatBenchmarkTraditionalCompare(res) {
|
|
574
|
+
const f = (n) => Number.isFinite(n) ? n.toFixed(3) : String(n);
|
|
575
|
+
const t = res.traditional;
|
|
576
|
+
const speedup = (relax) => {
|
|
577
|
+
if (!Number.isFinite(relax) || !Number.isFinite(t.callsPerSec) || t.callsPerSec <= 0) return "n/a";
|
|
578
|
+
const s = relax / t.callsPerSec;
|
|
579
|
+
return s.toFixed(2) + "x";
|
|
580
|
+
};
|
|
581
|
+
const traditionalBlock = [
|
|
582
|
+
`${t.name} (baseline)`,
|
|
583
|
+
` iterations: ${t.iterations} (warmup ${t.warmup})`,
|
|
584
|
+
` calls/sec: ${f(t.callsPerSec)}`,
|
|
585
|
+
` avg ms: ${f(t.avgLatencyMs)}`,
|
|
586
|
+
` min ms: ${f(t.minLatencyMs)}`,
|
|
587
|
+
` max ms: ${f(t.maxLatencyMs)}`
|
|
588
|
+
].join("\n");
|
|
589
|
+
const summary = [
|
|
590
|
+
"Speedup vs baseline (higher is better)",
|
|
591
|
+
` sync: ${speedup(res.relaxnative.sync.callsPerSec)}`,
|
|
592
|
+
` worker: ${speedup(res.relaxnative.worker.callsPerSec)}`
|
|
593
|
+
].join("\n");
|
|
594
|
+
return [
|
|
595
|
+
traditionalBlock,
|
|
596
|
+
"",
|
|
597
|
+
summary,
|
|
598
|
+
"",
|
|
599
|
+
formatBenchmarkResult(res.relaxnative.sync),
|
|
600
|
+
"",
|
|
601
|
+
formatBenchmarkResult(res.relaxnative.worker)
|
|
602
|
+
].join("\n");
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export {
|
|
606
|
+
getProjectRoot,
|
|
607
|
+
getRegistryRoot,
|
|
608
|
+
getInstalledPackageDir,
|
|
609
|
+
resolveRegistryImport,
|
|
610
|
+
readRelaxJson,
|
|
611
|
+
installPackage,
|
|
612
|
+
removePackage,
|
|
613
|
+
listPackages,
|
|
614
|
+
updateIndex,
|
|
615
|
+
discoverNativeTestFiles,
|
|
616
|
+
discoverNativeTestsFromFile,
|
|
617
|
+
discoverNativeTests,
|
|
618
|
+
formatNativeTestResults,
|
|
619
|
+
runNativeTests,
|
|
620
|
+
getSourceLine,
|
|
621
|
+
benchmarkNativeFunction,
|
|
622
|
+
benchmarkCompareSyncVsWorker,
|
|
623
|
+
benchmarkCompareTraditionalVsRelaxnative,
|
|
624
|
+
formatBenchmarkResult,
|
|
625
|
+
formatBenchmarkCompare,
|
|
626
|
+
formatBenchmarkTraditionalCompare
|
|
627
|
+
};
|