@coana-tech/cli 15.0.1 → 15.0.3
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/cli.mjs +593 -474
- package/package.json +1 -1
- package/reachability-analyzers-cli.mjs +14848 -13724
- package/repos/coana-tech/goana/bin/goana-darwin-amd64.gz +0 -0
- package/repos/coana-tech/goana/bin/goana-darwin-arm64.gz +0 -0
- package/repos/coana-tech/goana/bin/goana-linux-amd64.gz +0 -0
- package/repos/coana-tech/goana/bin/goana-linux-arm64.gz +0 -0
- package/repos/coana-tech/javap-service/javap-service.jar +0 -0
- package/repos/coana-tech/jelly-private/dist/bundle/jelly.js +25 -6
- package/repos/coana-tech/spar/runtime/dist/approx.js +1060 -0
- package/repos/coana-tech/spar/runtime/dist/hints.js +131 -0
- package/repos/coana-tech/spar/runtime/dist/hooks.js +172 -0
- package/repos/coana-tech/spar/runtime/dist/logger.js +66 -0
- package/repos/coana-tech/spar/runtime/dist/options.js +10 -0
- package/repos/coana-tech/spar/runtime/dist/proxy.js +119 -0
- package/repos/coana-tech/spar/runtime/dist/sandbox.js +84 -0
- package/repos/coana-tech/spar/runtime/dist/types.js +3 -0
- package/repos/coana-tech/spar/sparjs-aarch64-apple-darwin.gz +0 -0
- package/repos/coana-tech/spar/sparjs-aarch64-unknown-linux-musl.gz +0 -0
- package/repos/coana-tech/spar/sparjs-x86_64-apple-darwin.gz +0 -0
- package/repos/coana-tech/spar/sparjs-x86_64-unknown-linux-musl.gz +0 -0
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
// Approximate interpretation runtime for SPAR.
|
|
2
|
+
// Adapted from Jelly's src/approx/approx.ts.
|
|
3
|
+
//
|
|
4
|
+
// Code transformation is delegated to the Rust parent process via FIFO/stdin IPC.
|
|
5
|
+
// Logging goes to stdout (inherited).
|
|
6
|
+
import { dirname, resolve } from "path";
|
|
7
|
+
import Module, { createRequire } from "module";
|
|
8
|
+
import { Hints } from "./hints.js";
|
|
9
|
+
import { isProxy, makeBaseProxy, makeModuleProxy, stdlibProxy, theArgumentsProxy, theProxy } from "./proxy.js";
|
|
10
|
+
import logger, { logToFile, writeStdOutIfActive } from "./logger.js";
|
|
11
|
+
import { options } from "./options.js";
|
|
12
|
+
import { pathToFileURL } from "url";
|
|
13
|
+
import { patchGlobalBuiltins, WHITELISTED } from "./sandbox.js";
|
|
14
|
+
import { inspect } from "util";
|
|
15
|
+
import { openSync, readSync, writeSync } from "fs";
|
|
16
|
+
import { MessageChannel } from "worker_threads";
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
// get options from parent process
|
|
19
|
+
Object.assign(options, JSON.parse(process.argv[2]));
|
|
20
|
+
// prepare logging
|
|
21
|
+
logger.level = options.loglevel;
|
|
22
|
+
if (options.logfile)
|
|
23
|
+
logToFile(options.logfile);
|
|
24
|
+
const PREFIX = "_J$";
|
|
25
|
+
/**
|
|
26
|
+
* Execution is aborted if loopCount reaches this limit.
|
|
27
|
+
*/
|
|
28
|
+
const LOOP_COUNT_LIMIT = 2500; // TODO: good value?
|
|
29
|
+
/**
|
|
30
|
+
* Execution is aborted if stackSize reaches this limit.
|
|
31
|
+
*/
|
|
32
|
+
const STACK_SIZE_LIMIT = 50; // TODO: good value?
|
|
33
|
+
/**
|
|
34
|
+
* Object allocation sites and types.
|
|
35
|
+
*/
|
|
36
|
+
const objLoc = new WeakMap();
|
|
37
|
+
/**
|
|
38
|
+
* Functions and classes discovered but not yet visited.
|
|
39
|
+
*/
|
|
40
|
+
const unvisitedFunctionsAndClasses = new Map();
|
|
41
|
+
/**
|
|
42
|
+
* Base objects for unvisited functions.
|
|
43
|
+
*/
|
|
44
|
+
const baseObjects = new Map();
|
|
45
|
+
/**
|
|
46
|
+
* Collected hints.
|
|
47
|
+
*/
|
|
48
|
+
const hints = new Hints();
|
|
49
|
+
/**
|
|
50
|
+
* Stack of dynamic properties in objects and classes.
|
|
51
|
+
* New entries are added by $init, contents added by $comp, used by $alloc.
|
|
52
|
+
*/
|
|
53
|
+
const constr = [];
|
|
54
|
+
/**
|
|
55
|
+
* Dynamic class instance fields for postponed write hints.
|
|
56
|
+
*/
|
|
57
|
+
const dynamicClassInstanceFields = new WeakMap();
|
|
58
|
+
/**
|
|
59
|
+
* Total number of times a loop body is entered.
|
|
60
|
+
*/
|
|
61
|
+
let loopCount = 0;
|
|
62
|
+
/**
|
|
63
|
+
* Call stack size (approximate).
|
|
64
|
+
*/
|
|
65
|
+
let stackSize = 0;
|
|
66
|
+
/**
|
|
67
|
+
* Total size of code (excluding dynamically generated code).
|
|
68
|
+
*/
|
|
69
|
+
let totalCodeSize = 0;
|
|
70
|
+
const NATIVE_CONSTRUCTORS = new Set([
|
|
71
|
+
Object, Boolean, Error, AggregateError, EvalError, RangeError, ReferenceError, SyntaxError, TypeError,
|
|
72
|
+
URIError, Number, BigInt, Date, String, RegExp, Array, Int8Array, Uint8Array, Uint8ClampedArray,
|
|
73
|
+
Int16Array, Uint16Array, Int32Array, Uint32Array, BigInt64Array, BigUint64Array, Float32Array,
|
|
74
|
+
Float64Array, Map, Set, WeakMap, WeakSet, ArrayBuffer, SharedArrayBuffer, DataView, Promise, Proxy
|
|
75
|
+
]);
|
|
76
|
+
/**
|
|
77
|
+
* Maps native constructors to their specific AllocType
|
|
78
|
+
* (matching ObjectKind in the static analysis).
|
|
79
|
+
*/
|
|
80
|
+
const NATIVE_ALLOC_TYPES = new Map([
|
|
81
|
+
[Map, "Map"], [Set, "Set"], [WeakMap, "WeakMap"], [WeakSet, "WeakSet"], [WeakRef, "WeakRef"],
|
|
82
|
+
[Promise, "Promise"], [Date, "Date"], [RegExp, "RegExp"],
|
|
83
|
+
[Error, "Error"], [AggregateError, "Error"], [EvalError, "Error"], [RangeError, "Error"],
|
|
84
|
+
[ReferenceError, "Error"], [SyntaxError, "Error"], [TypeError, "Error"], [URIError, "Error"],
|
|
85
|
+
]);
|
|
86
|
+
function getNativeAllocType(ctor, result) {
|
|
87
|
+
if (Array.isArray(result))
|
|
88
|
+
return "Array";
|
|
89
|
+
if (typeof result === "function")
|
|
90
|
+
return "Function";
|
|
91
|
+
return NATIVE_ALLOC_TYPES.get(ctor) ?? "Object";
|
|
92
|
+
}
|
|
93
|
+
function getLocationJSON(mod, loc) {
|
|
94
|
+
return `${hints.moduleIndex.get(mod) ?? "?"}:${loc}`;
|
|
95
|
+
}
|
|
96
|
+
function getProp(prop) {
|
|
97
|
+
return typeof prop === "symbol" ? String(prop) : `"${String(prop)}"`;
|
|
98
|
+
}
|
|
99
|
+
function getObjLoc(obj) {
|
|
100
|
+
return objLoc.get(obj) ?? [undefined, undefined];
|
|
101
|
+
}
|
|
102
|
+
function locToString(obj) {
|
|
103
|
+
const lt = objLoc.get(obj);
|
|
104
|
+
if (lt) {
|
|
105
|
+
const [loc, type] = lt;
|
|
106
|
+
return `${loc}:${type}`;
|
|
107
|
+
}
|
|
108
|
+
else
|
|
109
|
+
return "?";
|
|
110
|
+
}
|
|
111
|
+
function mapArrayAdd(key, value, map) {
|
|
112
|
+
let arr = map.get(key);
|
|
113
|
+
if (!arr) {
|
|
114
|
+
arr = [];
|
|
115
|
+
map.set(key, arr);
|
|
116
|
+
}
|
|
117
|
+
arr.push(value);
|
|
118
|
+
}
|
|
119
|
+
class ApproxError extends Error {
|
|
120
|
+
constructor(msg) {
|
|
121
|
+
super(msg);
|
|
122
|
+
}
|
|
123
|
+
toString() {
|
|
124
|
+
return `ApproxError: ${this.message}`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function incrementStackSize() {
|
|
128
|
+
if (stackSize++ > STACK_SIZE_LIMIT) {
|
|
129
|
+
stackSize = 0;
|
|
130
|
+
throw new ApproxError("Maximum stack size exceeded");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function decrementStackSize() {
|
|
134
|
+
stackSize--;
|
|
135
|
+
}
|
|
136
|
+
function handleException(ex) {
|
|
137
|
+
if (ex instanceof ApproxError || ex instanceof RangeError || ex.toString().startsWith("Error: Cannot find module"))
|
|
138
|
+
throw ex; // ensures that abort, stack overflow, and module load exceptions do not get swallowed
|
|
139
|
+
if (logger.isDebugEnabled())
|
|
140
|
+
logger.debug(`Suppressed exception: ${ex}`);
|
|
141
|
+
return theProxy;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Process pending write hints for class instance fields.
|
|
145
|
+
* @param fun function that has been instantiated
|
|
146
|
+
* @param res new instance
|
|
147
|
+
*/
|
|
148
|
+
function processPendingWriteHints(fun, res) {
|
|
149
|
+
const cs = dynamicClassInstanceFields.get(fun);
|
|
150
|
+
if (cs) {
|
|
151
|
+
for (const c of cs) {
|
|
152
|
+
const val = Object.getOwnPropertyDescriptor(res, c.prop)?.value;
|
|
153
|
+
const [baseLoc, baseType] = getObjLoc(res);
|
|
154
|
+
const [valLoc, valType] = getObjLoc(val);
|
|
155
|
+
if (baseLoc && baseType && valLoc && valType)
|
|
156
|
+
hints.addWriteHint({
|
|
157
|
+
type: "normal",
|
|
158
|
+
loc: getLocationJSON(c.mod, c.loc),
|
|
159
|
+
baseLoc,
|
|
160
|
+
baseType,
|
|
161
|
+
prop: c.prop,
|
|
162
|
+
valLoc,
|
|
163
|
+
valType
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
dynamicClassInstanceFields.delete(fun); // once per class is enough
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Overriding of special native functions where the call location is needed.
|
|
171
|
+
* @param mod module name
|
|
172
|
+
* @param loc source location
|
|
173
|
+
* @param base base value (undefined if absent)
|
|
174
|
+
* @param fun function
|
|
175
|
+
* @param args arguments
|
|
176
|
+
* @param isNew true if constructor call
|
|
177
|
+
* @return if proceed is true then proceed with the call, otherwise return the result value
|
|
178
|
+
*/
|
|
179
|
+
function callPre(mod, loc, base, fun, args, isNew) {
|
|
180
|
+
if (fun === Function) {
|
|
181
|
+
const funargs = args.slice(0, args.length - 1);
|
|
182
|
+
const funbody = args[args.length - 1] ?? "";
|
|
183
|
+
let error = false;
|
|
184
|
+
for (const a of funargs)
|
|
185
|
+
if (typeof a !== "string") {
|
|
186
|
+
error = true;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
if (!error) {
|
|
190
|
+
const str = `function anonymous(${funargs.join(",")}){${funbody}}`;
|
|
191
|
+
if (logger.isVerboseEnabled())
|
|
192
|
+
logger.verbose(`Function ${mod}:${loc} (code length: ${str.length})`);
|
|
193
|
+
if (logger.isDebugEnabled())
|
|
194
|
+
logger.debug(str);
|
|
195
|
+
const transformed = requestTransform(`${mod}:eval[${loc}]`, String(funbody), "commonjs");
|
|
196
|
+
const result = fun(...funargs, transformed);
|
|
197
|
+
hints.addEvalHint({
|
|
198
|
+
loc: getLocationJSON(mod, loc),
|
|
199
|
+
str
|
|
200
|
+
});
|
|
201
|
+
objLoc.set(result, [getLocationJSON(mod, loc), "Function"]);
|
|
202
|
+
return { proceed: false, result };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else if (!isNew)
|
|
206
|
+
if (fun.name === "require" && "resolve" in fun && "cache" in fun) { // probably a require function
|
|
207
|
+
const str = typeof args[0] === "string" && args[0].startsWith("node:") ? args[0].substring(5) : args[0];
|
|
208
|
+
if (Module.isBuiltin(str) && !WHITELISTED.has(str)) {
|
|
209
|
+
if (logger.isDebugEnabled())
|
|
210
|
+
logger.debug(`Intercepting require "${args[0]}"`);
|
|
211
|
+
return { proceed: false, result: stdlibProxy(fun(args[0])) };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else
|
|
215
|
+
switch (fun) {
|
|
216
|
+
case eval:
|
|
217
|
+
const str = args[0];
|
|
218
|
+
if (logger.isVerboseEnabled())
|
|
219
|
+
logger.verbose(`Indirect eval ${mod}:${loc} (code length: ${typeof str === "string" ? str.length : "?"})`);
|
|
220
|
+
if (typeof str === "string")
|
|
221
|
+
hints.addEvalHint({
|
|
222
|
+
loc: getLocationJSON(mod, loc),
|
|
223
|
+
str
|
|
224
|
+
});
|
|
225
|
+
const transformed = requestTransform(`${mod}:eval[${loc}]`, str, "commonjs");
|
|
226
|
+
const result = fun(transformed);
|
|
227
|
+
return { proceed: false, result };
|
|
228
|
+
case Function.prototype.apply:
|
|
229
|
+
return callPre(mod, loc, args[0], base, args[1] ?? [], false);
|
|
230
|
+
case Function.prototype.call:
|
|
231
|
+
return callPre(mod, loc, args[0], base, args.slice(1), false);
|
|
232
|
+
case Reflect.apply:
|
|
233
|
+
return callPre(mod, loc, args[1], args[0], args[2], false);
|
|
234
|
+
case Reflect.construct:
|
|
235
|
+
return callPre(mod, loc, args[1], args[0], args[2], true);
|
|
236
|
+
}
|
|
237
|
+
return { proceed: true };
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Post-processing of special native functions.
|
|
241
|
+
* @param mod module name
|
|
242
|
+
* @param loc source location
|
|
243
|
+
* @param fun function
|
|
244
|
+
* @param args arguments
|
|
245
|
+
* @param val result value
|
|
246
|
+
* @param base the receiver object, if method call
|
|
247
|
+
*/
|
|
248
|
+
function callPost(mod, loc, fun, args, val, base) {
|
|
249
|
+
/**
|
|
250
|
+
* Copies properties according to a property descriptor.
|
|
251
|
+
* @param to object to copy to
|
|
252
|
+
* @param prop property
|
|
253
|
+
* @param descriptor property descriptor
|
|
254
|
+
*/
|
|
255
|
+
function copyFromDescriptor(to, prop, descriptor) {
|
|
256
|
+
const [baseLoc, baseType] = getObjLoc(to);
|
|
257
|
+
if (baseLoc && baseType) {
|
|
258
|
+
if ("value" in descriptor) {
|
|
259
|
+
const [valLoc, valType] = getObjLoc(descriptor.value);
|
|
260
|
+
if (valLoc && valType)
|
|
261
|
+
hints.addWriteHint({
|
|
262
|
+
type: "normal",
|
|
263
|
+
loc: getLocationJSON(mod, loc),
|
|
264
|
+
baseLoc,
|
|
265
|
+
baseType,
|
|
266
|
+
prop,
|
|
267
|
+
valLoc,
|
|
268
|
+
valType
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if ("get" in descriptor) {
|
|
272
|
+
const [valLoc, valType] = getObjLoc(descriptor.get);
|
|
273
|
+
if (valLoc && valType)
|
|
274
|
+
hints.addWriteHint({
|
|
275
|
+
type: "get",
|
|
276
|
+
loc: getLocationJSON(mod, loc),
|
|
277
|
+
baseLoc,
|
|
278
|
+
baseType,
|
|
279
|
+
prop,
|
|
280
|
+
valLoc,
|
|
281
|
+
valType
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
if ("set" in descriptor) {
|
|
285
|
+
const [valLoc, valType] = getObjLoc(descriptor.set);
|
|
286
|
+
if (valLoc && valType)
|
|
287
|
+
hints.addWriteHint({ type: "set", loc: getLocationJSON(mod, loc), baseLoc, baseType, prop, valLoc, valType });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
switch (fun) {
|
|
292
|
+
case Object.create: {
|
|
293
|
+
objLoc.set(val, [getLocationJSON(mod, loc), "Object"]);
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case Object.assign: {
|
|
297
|
+
const target = args.at(0);
|
|
298
|
+
for (const arg of args.slice(1))
|
|
299
|
+
for (const [prop, val] of Object.entries(Object.getOwnPropertyDescriptors(arg)))
|
|
300
|
+
if (val.enumerable)
|
|
301
|
+
copyFromDescriptor(target, prop, val);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
case Object.defineProperty: {
|
|
305
|
+
copyFromDescriptor(args.at(0), args.at(1), args.at(2));
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case Object.defineProperties: {
|
|
309
|
+
const target = args.at(0);
|
|
310
|
+
for (const [prop, val] of Object.entries(args.at(1)))
|
|
311
|
+
copyFromDescriptor(target, prop, val);
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case Array.from:
|
|
315
|
+
case Array.of:
|
|
316
|
+
case Array.prototype.concat:
|
|
317
|
+
case Array.prototype.flat:
|
|
318
|
+
case Array.prototype.filter:
|
|
319
|
+
case Array.prototype.slice: {
|
|
320
|
+
objLoc.set(val, [getLocationJSON(mod, loc), "Array"]);
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
case Function.prototype.bind: {
|
|
324
|
+
const [baseLoc, baseAllocType] = getObjLoc(base);
|
|
325
|
+
if (!baseLoc || !baseAllocType)
|
|
326
|
+
return;
|
|
327
|
+
objLoc.set(val, [baseLoc, baseAllocType]);
|
|
328
|
+
baseObjects.set(baseLoc, args[0]);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
case Reflect.get: {
|
|
332
|
+
// TODO: produce read hint for Reflect.get
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
case Reflect.set: {
|
|
336
|
+
// TODO: produce write hint for Reflect.set
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
case Reflect.defineProperty: {
|
|
340
|
+
// TODO: produce write hint for Reflect.defineProperty
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// SPAR_IPC_FIFO: Node.js → Rust (transform requests, execution results)
|
|
346
|
+
const ipcFifoPath = process.env.SPAR_IPC_FIFO;
|
|
347
|
+
if (!ipcFifoPath)
|
|
348
|
+
throw new Error("SPAR_IPC_FIFO environment variable not set");
|
|
349
|
+
const ipcFd = openSync(ipcFifoPath, "w");
|
|
350
|
+
// Synchronous line read from stdin (fd 0).
|
|
351
|
+
// stdin: Rust → Node.js main thread (file requests, CJS transform responses)
|
|
352
|
+
// Used for receiving transform responses and file requests.
|
|
353
|
+
const stdinBuffer = Buffer.alloc(65536);
|
|
354
|
+
let stdinLeftover = "";
|
|
355
|
+
function readLineSync() {
|
|
356
|
+
while (true) {
|
|
357
|
+
const nlIdx = stdinLeftover.indexOf('\n');
|
|
358
|
+
if (nlIdx !== -1) {
|
|
359
|
+
const line = stdinLeftover.substring(0, nlIdx);
|
|
360
|
+
stdinLeftover = stdinLeftover.substring(nlIdx + 1);
|
|
361
|
+
return line;
|
|
362
|
+
}
|
|
363
|
+
const bytesRead = readSync(0, stdinBuffer, 0, stdinBuffer.length, null);
|
|
364
|
+
if (bytesRead === 0)
|
|
365
|
+
throw new Error("stdin closed unexpectedly");
|
|
366
|
+
stdinLeftover += stdinBuffer.toString("utf8", 0, bytesRead);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function sendToParent(msg) {
|
|
370
|
+
const json = JSON.stringify(msg) + '\n';
|
|
371
|
+
writeSync(ipcFd, json);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Request code transformation from the Rust parent.
|
|
375
|
+
* Synchronous: sends request to stdout, reads response from stdin.
|
|
376
|
+
* @param file module name
|
|
377
|
+
* @param source the code
|
|
378
|
+
* @param mode CJS/ESM
|
|
379
|
+
* @return transformed code, or "" if transformation failed
|
|
380
|
+
*/
|
|
381
|
+
function requestTransform(file, source, mode) {
|
|
382
|
+
sendToParent({ transform: file, source, sourceType: mode });
|
|
383
|
+
const line = readLineSync();
|
|
384
|
+
const resp = JSON.parse(line);
|
|
385
|
+
return resp.transformed;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Instruments a CJS source file by requesting transformation from the Rust parent.
|
|
389
|
+
* @param filename file path
|
|
390
|
+
* @param code the code
|
|
391
|
+
*/
|
|
392
|
+
function transformModule(filename, code) {
|
|
393
|
+
if (!(typeof filename === "string" && typeof code === "string"))
|
|
394
|
+
return ""; // value likely generated by the proxy, ignore
|
|
395
|
+
// Resolve symlinks for basedir comparison
|
|
396
|
+
let resolvedFilename = filename;
|
|
397
|
+
try {
|
|
398
|
+
resolvedFilename = require("fs").realpathSync(filename);
|
|
399
|
+
}
|
|
400
|
+
catch { }
|
|
401
|
+
let resolvedBasedir = options.basedir;
|
|
402
|
+
try {
|
|
403
|
+
resolvedBasedir = require("fs").realpathSync(options.basedir);
|
|
404
|
+
}
|
|
405
|
+
catch { }
|
|
406
|
+
if (!resolvedFilename.startsWith(resolvedBasedir)) {
|
|
407
|
+
if (logger.isVerboseEnabled())
|
|
408
|
+
logger.verbose(`Ignoring module outside basedir: ${filename}`);
|
|
409
|
+
return `module.exports = ${PREFIX}proxy`;
|
|
410
|
+
}
|
|
411
|
+
writeStdOutIfActive(`Loading module ${filename} (${Math.ceil(code.length / 1024)}KB)`);
|
|
412
|
+
if (logger.isVerboseEnabled())
|
|
413
|
+
logger.verbose(`Instrumenting ${filename}`);
|
|
414
|
+
totalCodeSize += code.length;
|
|
415
|
+
return requestTransform(filename, code, "commonjs");
|
|
416
|
+
}
|
|
417
|
+
const g = globalThis;
|
|
418
|
+
for (const [name, val] of Object.entries({
|
|
419
|
+
/**
|
|
420
|
+
* The proxy mock object.
|
|
421
|
+
*/
|
|
422
|
+
proxy: theProxy,
|
|
423
|
+
/**
|
|
424
|
+
* Sandboxed builtin modules (used by hooks.ts).
|
|
425
|
+
*/
|
|
426
|
+
builtin: Object.fromEntries(Module.builtinModules
|
|
427
|
+
.filter(m => !WHITELISTED.has(m))
|
|
428
|
+
.map(m => [m, stdlibProxy(require(m))])),
|
|
429
|
+
/**
|
|
430
|
+
* Records the entry of a module.
|
|
431
|
+
* Also wraps the module object to prevent access to module.constructor.
|
|
432
|
+
*/
|
|
433
|
+
start(mod, modobj) {
|
|
434
|
+
const i = hints.addModule(mod);
|
|
435
|
+
if (logger.isDebugEnabled())
|
|
436
|
+
logger.debug(`$start ${mod}: ${i}`);
|
|
437
|
+
if (modobj && modobj.exports) { // undefined for ESM modules (don't have dynamic exports anyway)
|
|
438
|
+
objLoc.set(modobj.exports, [`${i}`, "Object"]); // allocation site for module.exports
|
|
439
|
+
return makeModuleProxy(modobj); // FIXME: assigning to module fails in strict mode (suppressed exception)
|
|
440
|
+
}
|
|
441
|
+
return undefined; // undefined for ESM modules (don't have dynamic exports anyway)
|
|
442
|
+
},
|
|
443
|
+
/**
|
|
444
|
+
* Records the entry of an object expression or class.
|
|
445
|
+
*/
|
|
446
|
+
init() {
|
|
447
|
+
logger.debug("$init");
|
|
448
|
+
constr.push([]);
|
|
449
|
+
},
|
|
450
|
+
/**
|
|
451
|
+
* Records the exit of an object expression, class or function and collects the allocation sites.
|
|
452
|
+
* @param mod module name
|
|
453
|
+
* @param loc source location
|
|
454
|
+
* @param obj new object
|
|
455
|
+
* @param hasInit if true, the call matches a call to $init
|
|
456
|
+
* @param isClass if true, the object is a class constructor
|
|
457
|
+
* @return the new object
|
|
458
|
+
*/
|
|
459
|
+
alloc(mod, loc, obj, hasInit, isClass) {
|
|
460
|
+
if (typeof obj === "object" || typeof obj === "function" || Array.isArray(obj)) {
|
|
461
|
+
if (logger.isDebugEnabled())
|
|
462
|
+
logger.debug(`$alloc ${mod}:${loc}: ${Array.isArray(obj) ? "array" : typeof obj}`);
|
|
463
|
+
const s = getLocationJSON(mod, loc);
|
|
464
|
+
if (Array.isArray(obj))
|
|
465
|
+
objLoc.set(obj, [s, "Array"]); // allocation site for array
|
|
466
|
+
else if (typeof obj === "object")
|
|
467
|
+
objLoc.set(obj, [s, "Object"]); // allocation site for object expression
|
|
468
|
+
else {
|
|
469
|
+
if (isClass)
|
|
470
|
+
objLoc.set(obj, [s, "Class"]); // allocation site for class
|
|
471
|
+
else
|
|
472
|
+
objLoc.set(obj, [s, "Function"]); // allocation site for function
|
|
473
|
+
if (obj.prototype)
|
|
474
|
+
objLoc.set(obj.prototype, [s, "Prototype"]); // allocation site for (non-arrow) function or class prototype
|
|
475
|
+
}
|
|
476
|
+
if (typeof obj === "function" && !hints.functions.has(s) && !unvisitedFunctionsAndClasses.has(s))
|
|
477
|
+
unvisitedFunctionsAndClasses.set(s, { fun: obj, isClass });
|
|
478
|
+
if (hasInit)
|
|
479
|
+
for (const c of constr.pop()) {
|
|
480
|
+
let type;
|
|
481
|
+
let valLoc, valType;
|
|
482
|
+
let baseLoc = s, baseType;
|
|
483
|
+
if (typeof obj === "function") { // class
|
|
484
|
+
baseType = c.isStatic ? "Class" : "Prototype";
|
|
485
|
+
const desc = Object.getOwnPropertyDescriptor(c.isStatic ? obj : obj.prototype, c.prop);
|
|
486
|
+
switch (c.kind) {
|
|
487
|
+
case "field":
|
|
488
|
+
if (!c.isStatic) {
|
|
489
|
+
// class instance field, need to postpone hint until location of value is known at 'new'
|
|
490
|
+
mapArrayAdd(obj, c, dynamicClassInstanceFields);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
type = "normal";
|
|
494
|
+
[valLoc, valType] = getObjLoc(desc?.value);
|
|
495
|
+
break;
|
|
496
|
+
case "method":
|
|
497
|
+
type = "normal";
|
|
498
|
+
valLoc = getLocationJSON(c.mod, c.loc);
|
|
499
|
+
valType = "Function";
|
|
500
|
+
const v = desc?.value;
|
|
501
|
+
if (v)
|
|
502
|
+
objLoc.set(v, [valLoc, valType]);
|
|
503
|
+
break;
|
|
504
|
+
case "get":
|
|
505
|
+
case "set":
|
|
506
|
+
type = c.kind;
|
|
507
|
+
valLoc = getLocationJSON(c.mod, c.loc);
|
|
508
|
+
valType = "Function";
|
|
509
|
+
const a = desc?.[c.kind];
|
|
510
|
+
if (a)
|
|
511
|
+
objLoc.set(a, [valLoc, valType]);
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
else { // object
|
|
516
|
+
baseType = "Object";
|
|
517
|
+
const desc = Object.getOwnPropertyDescriptor(obj, c.prop);
|
|
518
|
+
switch (c.kind) {
|
|
519
|
+
case "field":
|
|
520
|
+
type = "normal";
|
|
521
|
+
[valLoc, valType] = getObjLoc(desc?.value);
|
|
522
|
+
break;
|
|
523
|
+
case "method":
|
|
524
|
+
type = "normal";
|
|
525
|
+
valLoc = getLocationJSON(c.mod, c.loc);
|
|
526
|
+
valType = "Function";
|
|
527
|
+
const v = desc?.value;
|
|
528
|
+
if (v)
|
|
529
|
+
objLoc.set(v, [valLoc, valType]);
|
|
530
|
+
break;
|
|
531
|
+
case "get":
|
|
532
|
+
case "set":
|
|
533
|
+
type = c.kind;
|
|
534
|
+
valLoc = getLocationJSON(c.mod, c.loc);
|
|
535
|
+
valType = "Function";
|
|
536
|
+
const a = desc?.[c.kind];
|
|
537
|
+
if (a)
|
|
538
|
+
objLoc.set(a, [valLoc, valType]);
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (c.isDynamic && valLoc && valType)
|
|
543
|
+
hints.addWriteHint({
|
|
544
|
+
type,
|
|
545
|
+
loc: getLocationJSON(c.mod, c.loc),
|
|
546
|
+
baseLoc,
|
|
547
|
+
baseType,
|
|
548
|
+
prop: c.prop,
|
|
549
|
+
valLoc,
|
|
550
|
+
valType
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return obj;
|
|
555
|
+
},
|
|
556
|
+
/**
|
|
557
|
+
* Performs a (static or dynamic) property write operation and collects a write hint.
|
|
558
|
+
* @param mod module name
|
|
559
|
+
* @param loc source location
|
|
560
|
+
* @param base base value
|
|
561
|
+
* @param prop property value
|
|
562
|
+
* @param val value being assigned
|
|
563
|
+
* @param isDynamic if true, the property name is a computed value
|
|
564
|
+
* @return the value being assigned
|
|
565
|
+
*/
|
|
566
|
+
pw(mod, loc, base, prop, val, isDynamic) {
|
|
567
|
+
if (base === undefined) {
|
|
568
|
+
if (logger.isDebugEnabled())
|
|
569
|
+
logger.debug(`Suppressed exception: TypeError: Cannot set properties of undefined`);
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
if (typeof prop === "symbol" || Array.isArray(base))
|
|
573
|
+
return base[prop]; // ignoring symbols and writes to arrays
|
|
574
|
+
if (isProxy(base) || isProxy(val))
|
|
575
|
+
return theProxy;
|
|
576
|
+
const p = String(prop);
|
|
577
|
+
if (logger.isDebugEnabled())
|
|
578
|
+
logger.debug(`$pw ${mod}:${loc}: ${locToString(base)}${isDynamic ? `[${getProp(prop)}]` : `.${String(prop)}`} = ${locToString(val)}`);
|
|
579
|
+
try {
|
|
580
|
+
base[p] = val;
|
|
581
|
+
}
|
|
582
|
+
catch (ex) {
|
|
583
|
+
if (logger.isDebugEnabled())
|
|
584
|
+
logger.debug(`Suppressed exception: ${ex}`);
|
|
585
|
+
}
|
|
586
|
+
if (typeof val === "function") {
|
|
587
|
+
const loc = objLoc.get(val);
|
|
588
|
+
if (loc) {
|
|
589
|
+
const [funloc] = loc;
|
|
590
|
+
if (!baseObjects.has(funloc))
|
|
591
|
+
baseObjects.set(funloc, base);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const [baseLoc, baseType] = getObjLoc(base);
|
|
595
|
+
const [valLoc, valType] = getObjLoc(val);
|
|
596
|
+
if (baseLoc && baseType && valLoc && valType)
|
|
597
|
+
hints.addWriteHint({
|
|
598
|
+
type: "normal",
|
|
599
|
+
loc: getLocationJSON(mod, loc),
|
|
600
|
+
baseLoc,
|
|
601
|
+
baseType,
|
|
602
|
+
prop: p,
|
|
603
|
+
valLoc,
|
|
604
|
+
valType
|
|
605
|
+
});
|
|
606
|
+
return val;
|
|
607
|
+
},
|
|
608
|
+
/**
|
|
609
|
+
* Performs a dynamic property read operation and collects a read hint.
|
|
610
|
+
* @param mod module name
|
|
611
|
+
* @param loc source location
|
|
612
|
+
* @param base base value
|
|
613
|
+
* @param prop property value
|
|
614
|
+
* @return the result value
|
|
615
|
+
*/
|
|
616
|
+
dpr(mod, loc, base, prop) {
|
|
617
|
+
if (base === undefined) {
|
|
618
|
+
if (logger.isDebugEnabled())
|
|
619
|
+
logger.debug(`Suppressed exception: TypeError: Cannot read properties of undefined`);
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
622
|
+
if (Array.isArray(base))
|
|
623
|
+
return base[prop]; // ignoring reads from arrays
|
|
624
|
+
if (isProxy(base))
|
|
625
|
+
return theProxy;
|
|
626
|
+
const p = typeof prop === "symbol" ? prop : String(prop);
|
|
627
|
+
let val;
|
|
628
|
+
try {
|
|
629
|
+
val = base[p];
|
|
630
|
+
}
|
|
631
|
+
catch (ex) {
|
|
632
|
+
if (logger.isDebugEnabled())
|
|
633
|
+
logger.debug(`Suppressed exception: ${ex}`);
|
|
634
|
+
return theProxy;
|
|
635
|
+
}
|
|
636
|
+
if (isProxy(val))
|
|
637
|
+
return theProxy;
|
|
638
|
+
if (logger.isDebugEnabled())
|
|
639
|
+
logger.debug(`$dpr ${mod}:${loc}: ${locToString(base)}[${getProp(prop)}] -> ${locToString(val)}`);
|
|
640
|
+
const [valLoc, valType] = getObjLoc(val);
|
|
641
|
+
if (valLoc && valType)
|
|
642
|
+
hints.addReadHint({
|
|
643
|
+
loc: getLocationJSON(mod, loc),
|
|
644
|
+
prop: typeof p === "string" ? p : undefined,
|
|
645
|
+
valLoc,
|
|
646
|
+
valType
|
|
647
|
+
});
|
|
648
|
+
return val;
|
|
649
|
+
},
|
|
650
|
+
/**
|
|
651
|
+
* Performs a function call and models special native functions.
|
|
652
|
+
* @param mod module name
|
|
653
|
+
* @param loc source location
|
|
654
|
+
* @param fun function being called
|
|
655
|
+
* @param isOptionalCall if true, this is an optional call
|
|
656
|
+
* @param args arguments
|
|
657
|
+
* @return the call result value
|
|
658
|
+
*/
|
|
659
|
+
fun(mod, loc, fun, isOptionalCall, ...args) {
|
|
660
|
+
if (logger.isDebugEnabled())
|
|
661
|
+
logger.debug(`$fun ${mod}:${loc}${isOptionalCall ? " optional" : ""}`);
|
|
662
|
+
if (isOptionalCall && (fun === undefined || fun === null))
|
|
663
|
+
return undefined;
|
|
664
|
+
if (typeof fun !== "function")
|
|
665
|
+
return theProxy;
|
|
666
|
+
try {
|
|
667
|
+
incrementStackSize();
|
|
668
|
+
const { proceed, result } = callPre(mod, loc, undefined, fun, args, false);
|
|
669
|
+
if (proceed) {
|
|
670
|
+
const res = Reflect.apply(fun, undefined, args);
|
|
671
|
+
callPost(mod, loc, fun, args, res);
|
|
672
|
+
return res;
|
|
673
|
+
}
|
|
674
|
+
else
|
|
675
|
+
return result;
|
|
676
|
+
}
|
|
677
|
+
catch (ex) {
|
|
678
|
+
return handleException(ex);
|
|
679
|
+
}
|
|
680
|
+
finally {
|
|
681
|
+
decrementStackSize();
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
/**
|
|
685
|
+
* Performs a method call and models special native functions.
|
|
686
|
+
* @param mod module name
|
|
687
|
+
* @param loc source location
|
|
688
|
+
* @param base base value
|
|
689
|
+
* @param prop property value
|
|
690
|
+
* @param isDynamic if true, the method name is a computed value
|
|
691
|
+
* @param isOptionalMember if true, the method expression is an optional member expression
|
|
692
|
+
* @param isOptionalCall if true, this is an optional call
|
|
693
|
+
* @param args arguments
|
|
694
|
+
* @return the call result value
|
|
695
|
+
*/
|
|
696
|
+
method(mod, loc, base, prop, isDynamic, isOptionalMember, isOptionalCall, ...args) {
|
|
697
|
+
if (logger.isDebugEnabled())
|
|
698
|
+
logger.debug(`$method ${mod}:${loc}${isDynamic ? " dynamic" : ""}${isOptionalMember ? " optionalMember" : ""}${isOptionalCall ? " optionalCall" : ""}`);
|
|
699
|
+
let fun;
|
|
700
|
+
try {
|
|
701
|
+
fun = isOptionalMember && (base === undefined || base === null) ? undefined : base[prop];
|
|
702
|
+
}
|
|
703
|
+
catch (ex) {
|
|
704
|
+
if (logger.isDebugEnabled())
|
|
705
|
+
logger.debug(`Suppressed exception: ${ex}`);
|
|
706
|
+
return theProxy;
|
|
707
|
+
}
|
|
708
|
+
if (isOptionalCall && (fun === undefined || fun === null))
|
|
709
|
+
return undefined;
|
|
710
|
+
if (typeof fun !== "function") {
|
|
711
|
+
if (logger.isDebugEnabled())
|
|
712
|
+
logger.debug(`Suppressed exception: TypeError: Must be a function`);
|
|
713
|
+
return theProxy;
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
incrementStackSize();
|
|
717
|
+
const { proceed, result } = callPre(mod, loc, base, fun, args, false);
|
|
718
|
+
if (proceed) {
|
|
719
|
+
const res = Reflect.apply(fun, base, args);
|
|
720
|
+
callPost(mod, loc, fun, args, res, base);
|
|
721
|
+
return res;
|
|
722
|
+
}
|
|
723
|
+
else
|
|
724
|
+
return result;
|
|
725
|
+
}
|
|
726
|
+
catch (ex) {
|
|
727
|
+
return handleException(ex);
|
|
728
|
+
}
|
|
729
|
+
finally {
|
|
730
|
+
decrementStackSize();
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
/**
|
|
734
|
+
* Performs a 'new' operation.
|
|
735
|
+
* @param mod module name
|
|
736
|
+
* @param loc source location
|
|
737
|
+
* @param fun the constructor to instantiate
|
|
738
|
+
* @param args arguments
|
|
739
|
+
* @return the result value
|
|
740
|
+
*/
|
|
741
|
+
new(mod, loc, fun, ...args) {
|
|
742
|
+
logger.debug("$new");
|
|
743
|
+
if (typeof fun !== "function") {
|
|
744
|
+
if (logger.isDebugEnabled())
|
|
745
|
+
logger.debug(`Suppressed exception: TypeError: Must be a function`);
|
|
746
|
+
return theProxy;
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
incrementStackSize();
|
|
750
|
+
const { proceed, result } = callPre(mod, loc, undefined, fun, args, true);
|
|
751
|
+
if (proceed) {
|
|
752
|
+
const res = Reflect.construct(fun, args);
|
|
753
|
+
// For implicit constructors (no _J$this), set objLoc on the instance
|
|
754
|
+
// using the class's own objLoc (set by _J$alloc).
|
|
755
|
+
if (typeof res === "object" && res !== null && !objLoc.has(res)) {
|
|
756
|
+
const [classLoc] = getObjLoc(fun);
|
|
757
|
+
if (classLoc)
|
|
758
|
+
objLoc.set(res, [classLoc, "Object"]);
|
|
759
|
+
}
|
|
760
|
+
processPendingWriteHints(fun, res);
|
|
761
|
+
if (NATIVE_CONSTRUCTORS.has(fun) && (typeof res === "object" || typeof res === "function")) {
|
|
762
|
+
const t = getNativeAllocType(fun, res);
|
|
763
|
+
if (t)
|
|
764
|
+
objLoc.set(res, [getLocationJSON(mod, loc), t]);
|
|
765
|
+
}
|
|
766
|
+
return res;
|
|
767
|
+
}
|
|
768
|
+
else
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
catch (ex) {
|
|
772
|
+
return handleException(ex);
|
|
773
|
+
}
|
|
774
|
+
finally {
|
|
775
|
+
decrementStackSize();
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
/**
|
|
779
|
+
* Records a dynamic property to be processed later by $alloc.
|
|
780
|
+
* @param mod module name
|
|
781
|
+
* @param loc source location
|
|
782
|
+
* @param prop property value
|
|
783
|
+
* @param kind kind of property
|
|
784
|
+
* @param isStatic if true this is a static field
|
|
785
|
+
* @param isDynamic if true, the property name is a computed value
|
|
786
|
+
* @return the property value
|
|
787
|
+
*/
|
|
788
|
+
comp(mod, loc, prop, kind, isStatic, isDynamic) {
|
|
789
|
+
if (logger.isDebugEnabled())
|
|
790
|
+
logger.debug(`$comp ${mod}:${loc} ${getProp(prop)} ${kind}`);
|
|
791
|
+
if (typeof prop !== "symbol")
|
|
792
|
+
constr.at(constr.length - 1).push({ mod, loc, prop: String(prop), kind, isStatic, isDynamic });
|
|
793
|
+
return prop;
|
|
794
|
+
},
|
|
795
|
+
/**
|
|
796
|
+
* Registers that a function or class constructor has been visited.
|
|
797
|
+
* @param mod module name
|
|
798
|
+
* @param loc source location
|
|
799
|
+
*/
|
|
800
|
+
enter(mod, loc) {
|
|
801
|
+
if (logger.isDebugEnabled())
|
|
802
|
+
logger.debug(`$enter ${mod}:${loc}`);
|
|
803
|
+
const s = getLocationJSON(mod, loc);
|
|
804
|
+
unvisitedFunctionsAndClasses.delete(s);
|
|
805
|
+
hints.addFunction(s);
|
|
806
|
+
},
|
|
807
|
+
/**
|
|
808
|
+
* Registers 'this' in a function or class constructor.
|
|
809
|
+
* @param mod module name
|
|
810
|
+
* @param loc source location
|
|
811
|
+
* @param thiss the 'this' object
|
|
812
|
+
* @returns the 'this' object
|
|
813
|
+
*/
|
|
814
|
+
this(mod, loc, thiss) {
|
|
815
|
+
logger.debug(`$this ${mod}:${loc}`);
|
|
816
|
+
if (thiss) {
|
|
817
|
+
const s = getLocationJSON(mod, loc);
|
|
818
|
+
objLoc.set(thiss, [s, "Object"]); // allocation site for 'new' expression
|
|
819
|
+
}
|
|
820
|
+
return thiss;
|
|
821
|
+
},
|
|
822
|
+
/**
|
|
823
|
+
* Invoked when entering a catch block to make sure AbortExceptions are passed through.
|
|
824
|
+
* @param ex the exception
|
|
825
|
+
*/
|
|
826
|
+
catch(ex) {
|
|
827
|
+
logger.debug("$catch");
|
|
828
|
+
if (ex instanceof ApproxError)
|
|
829
|
+
throw ex; // ensures that abort exceptions do not get swallowed
|
|
830
|
+
},
|
|
831
|
+
/**
|
|
832
|
+
* Invoked when entering a loop body to terminate long-running executions.
|
|
833
|
+
*/
|
|
834
|
+
loop() {
|
|
835
|
+
logger.debug("$loop");
|
|
836
|
+
if (loopCount++ > LOOP_COUNT_LIMIT) {
|
|
837
|
+
loopCount = 0;
|
|
838
|
+
throw new ApproxError("Loop limit reached");
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
/**
|
|
842
|
+
* Records a direct eval call and instruments the code.
|
|
843
|
+
* @param mod module name
|
|
844
|
+
* @param loc source location
|
|
845
|
+
* @param str eval string
|
|
846
|
+
* @return the instrumented eval string
|
|
847
|
+
*/
|
|
848
|
+
eval(mod, loc, str) {
|
|
849
|
+
if (logger.isDebugEnabled())
|
|
850
|
+
logger.debug(`$eval ${mod}:${loc} (code length: ${typeof str === "string" ? str.length : "?"})`);
|
|
851
|
+
if (typeof str === "string")
|
|
852
|
+
hints.addEvalHint({
|
|
853
|
+
loc: getLocationJSON(mod, loc),
|
|
854
|
+
str
|
|
855
|
+
});
|
|
856
|
+
return requestTransform(`${mod}:eval[${loc}]`, str, "commonjs");
|
|
857
|
+
},
|
|
858
|
+
/**
|
|
859
|
+
* Records a dynamic require/import.
|
|
860
|
+
* @param mod name of module containing the require/import
|
|
861
|
+
* @param loc source location
|
|
862
|
+
* @param str module string
|
|
863
|
+
* @return the module string
|
|
864
|
+
*/
|
|
865
|
+
require(mod, loc, str) {
|
|
866
|
+
if (Module.isBuiltin(str))
|
|
867
|
+
return str;
|
|
868
|
+
if (logger.isDebugEnabled())
|
|
869
|
+
logger.debug(`$require ${mod}:${loc} "${str}"`);
|
|
870
|
+
if (typeof str === "string")
|
|
871
|
+
hints.addRequireHint({
|
|
872
|
+
loc: getLocationJSON(mod, loc),
|
|
873
|
+
str
|
|
874
|
+
});
|
|
875
|
+
return str;
|
|
876
|
+
},
|
|
877
|
+
/**
|
|
878
|
+
* Freezes the given object.
|
|
879
|
+
*/
|
|
880
|
+
freeze(obj) {
|
|
881
|
+
Object.freeze(obj);
|
|
882
|
+
}
|
|
883
|
+
}))
|
|
884
|
+
g[PREFIX + name] = val;
|
|
885
|
+
/**
|
|
886
|
+
* Log function for testing and debugging.
|
|
887
|
+
* @param msg message
|
|
888
|
+
*/
|
|
889
|
+
g.$log = function (msg) {
|
|
890
|
+
writeStdOutIfActive("");
|
|
891
|
+
logger.info(`$log: ${inspect(msg, { depth: 1 })}`);
|
|
892
|
+
};
|
|
893
|
+
const realSetTimeout = setTimeout;
|
|
894
|
+
/**
|
|
895
|
+
* Performs forced execution of functions that have been found but not visited.
|
|
896
|
+
*/
|
|
897
|
+
async function forceExecuteUnvisitedFunctions() {
|
|
898
|
+
let numForced = 0, numForcedExceptions = 0;
|
|
899
|
+
for (const [loc, { fun, isClass }] of unvisitedFunctionsAndClasses) {
|
|
900
|
+
const sloc = `${hints.modules[parseInt(loc)]}${loc.substring(loc.indexOf(":"))}`;
|
|
901
|
+
const msg = `Force-executing ${isClass ? "constructor" : "function"} ${sloc} (${unvisitedFunctionsAndClasses.size - 1} pending)`;
|
|
902
|
+
if (logger.isVerboseEnabled())
|
|
903
|
+
logger.verbose(msg);
|
|
904
|
+
else
|
|
905
|
+
writeStdOutIfActive(msg);
|
|
906
|
+
try {
|
|
907
|
+
const args = theArgumentsProxy;
|
|
908
|
+
if (isClass)
|
|
909
|
+
Reflect.construct(fun, args);
|
|
910
|
+
else {
|
|
911
|
+
const base = baseObjects.get(loc);
|
|
912
|
+
let res = Reflect.apply(fun, makeBaseProxy(base), args);
|
|
913
|
+
if (res && typeof res === "object" && (Symbol.iterator in res || Symbol.asyncIterator in res) && typeof res.next === "function") // fun is a generator function
|
|
914
|
+
res.next(); // TODO: currently only invoking 'next' once
|
|
915
|
+
if (res instanceof Promise) {
|
|
916
|
+
if (logger.isDebugEnabled())
|
|
917
|
+
logger.debug("Awaiting promise");
|
|
918
|
+
res = await Promise.race([res, new Promise(resolve => realSetTimeout(resolve, 100))]);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (logger.isDebugEnabled())
|
|
922
|
+
logger.debug("Function completed successfully");
|
|
923
|
+
}
|
|
924
|
+
catch (err) {
|
|
925
|
+
if (logger.isVerboseEnabled())
|
|
926
|
+
logger.verbose(`Function completed with exception: ${err instanceof Error && logger.isDebugEnabled() ? err.stack : err}`);
|
|
927
|
+
numForcedExceptions++;
|
|
928
|
+
}
|
|
929
|
+
// Skip check for classes without explicit constructors — there is no constructor body
|
|
930
|
+
// to instrument with _J$enter, so the location is never added to hints.functions.
|
|
931
|
+
// (Jelly avoids this by inserting synthetic constructors during Babel preprocessing.)
|
|
932
|
+
if (!isClass && !hints.functions.has(loc)) {
|
|
933
|
+
const sloc = `${hints.modules[parseInt(loc)]}${loc.substring(loc.indexOf(":"))}`;
|
|
934
|
+
logger.error(`Error: Function ${sloc} should be visited now`);
|
|
935
|
+
}
|
|
936
|
+
numForced++;
|
|
937
|
+
unvisitedFunctionsAndClasses.delete(loc);
|
|
938
|
+
baseObjects.delete(loc);
|
|
939
|
+
loopCount = 0;
|
|
940
|
+
}
|
|
941
|
+
return { numForced, numForcedExceptions };
|
|
942
|
+
}
|
|
943
|
+
// intercept ESM module loading
|
|
944
|
+
// SPAR_HOOKS_FIFO: Rust → Node.js hooks thread (ESM transform responses)
|
|
945
|
+
const { port1, port2 } = new MessageChannel();
|
|
946
|
+
Module.register("./hooks.js", {
|
|
947
|
+
parentURL: import.meta.url,
|
|
948
|
+
data: {
|
|
949
|
+
opts: options,
|
|
950
|
+
port2,
|
|
951
|
+
ipcFifoPath,
|
|
952
|
+
hooksFifoPath: process.env.SPAR_HOOKS_FIFO,
|
|
953
|
+
},
|
|
954
|
+
transferList: [port2]
|
|
955
|
+
});
|
|
956
|
+
port1.on("message", (msg) => {
|
|
957
|
+
switch (msg.type) {
|
|
958
|
+
case "log":
|
|
959
|
+
logger[msg.level]?.(msg.str);
|
|
960
|
+
break;
|
|
961
|
+
case "metadata":
|
|
962
|
+
totalCodeSize += msg.codeSize;
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
// intercept CJS module loading
|
|
967
|
+
const realCompile = Module.prototype._compile;
|
|
968
|
+
Module.prototype._compile = function (content, filename) {
|
|
969
|
+
if (typeof content !== "string" || typeof filename !== "string")
|
|
970
|
+
return; // protect against accidental calls
|
|
971
|
+
if (logger.isVerboseEnabled())
|
|
972
|
+
logger.verbose(`Loading ${filename} (CJS loader)`);
|
|
973
|
+
content = transformModule(filename, content);
|
|
974
|
+
try {
|
|
975
|
+
return realCompile.call(this, content, filename);
|
|
976
|
+
}
|
|
977
|
+
catch (err) {
|
|
978
|
+
if (String(err).includes("SyntaxError"))
|
|
979
|
+
logger.verbose(`Unable to load ${filename} (trying to load ESM module as CJS?)`); // TODO: retry using ESM loader?
|
|
980
|
+
else
|
|
981
|
+
logger.warn(`Unable to load ${filename}: ${err instanceof Error && logger.isDebugEnabled() ? err.stack : err}`);
|
|
982
|
+
return realCompile.call(this, `module.exports = ${PREFIX}proxy`, filename);
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
// detect uncaught exceptions
|
|
986
|
+
process.on('uncaughtException', (err) => {
|
|
987
|
+
logger.warn(`Unexpected exception (insufficient sandboxing?): ${err instanceof Error && logger.isDebugEnabled() ? err.stack : err}`); // should not happen if sandboxing is done properly
|
|
988
|
+
});
|
|
989
|
+
process.on("unhandledRejection", (err) => {
|
|
990
|
+
logger.verbose(`Unhandled promise rejection: ${err instanceof Error ? err.stack : err}`); // (usually harmless)
|
|
991
|
+
});
|
|
992
|
+
const chdir = process.chdir.bind(process);
|
|
993
|
+
const dynamicImport = new Function("s", "return import(s)"); // prevents ts compilation to require
|
|
994
|
+
// In ESM, 'module' is not defined. Create a fake module object for CJS compatibility.
|
|
995
|
+
const fakeModule = {
|
|
996
|
+
filename: import.meta.filename ?? "",
|
|
997
|
+
path: import.meta.dirname ?? "",
|
|
998
|
+
paths: [],
|
|
999
|
+
exports: {},
|
|
1000
|
+
};
|
|
1001
|
+
// evaluate the code received from the master process, force execute unvisited functions, and return the resulting hints
|
|
1002
|
+
async function main() {
|
|
1003
|
+
while (true) {
|
|
1004
|
+
let line;
|
|
1005
|
+
try {
|
|
1006
|
+
line = readLineSync();
|
|
1007
|
+
}
|
|
1008
|
+
catch {
|
|
1009
|
+
break; // stdin closed
|
|
1010
|
+
}
|
|
1011
|
+
let msg;
|
|
1012
|
+
try {
|
|
1013
|
+
msg = JSON.parse(line);
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
logger.error(`Invalid JSON from parent: ${line}`);
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (!("file" in msg)) {
|
|
1020
|
+
// Not a file request (could be something else in the future)
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
logger.verbose(`Starting approximate interpretation of ${msg.file}`);
|
|
1024
|
+
let moduleException = false;
|
|
1025
|
+
fakeModule.filename = msg.file;
|
|
1026
|
+
fakeModule.path = dirname(msg.file);
|
|
1027
|
+
fakeModule.paths = [resolve(dirname(msg.file), "node_modules")];
|
|
1028
|
+
chdir(dirname(msg.file));
|
|
1029
|
+
try {
|
|
1030
|
+
await dynamicImport(pathToFileURL(msg.file));
|
|
1031
|
+
if (logger.isDebugEnabled())
|
|
1032
|
+
logger.debug(`Module completed successfully: ${msg.file}`);
|
|
1033
|
+
}
|
|
1034
|
+
catch (err) {
|
|
1035
|
+
if (logger.isVerboseEnabled())
|
|
1036
|
+
logger.verbose(`Uncaught exception for ${msg.file}: ${err instanceof Error && logger.isDebugEnabled() ? err.stack : err}`);
|
|
1037
|
+
moduleException = true;
|
|
1038
|
+
}
|
|
1039
|
+
loopCount = 0;
|
|
1040
|
+
const { numForced, numForcedExceptions } = await forceExecuteUnvisitedFunctions();
|
|
1041
|
+
logger.verbose("Approximate interpretation completed");
|
|
1042
|
+
sendToParent({
|
|
1043
|
+
hints: hints.toJSON(),
|
|
1044
|
+
numForced,
|
|
1045
|
+
numForcedExceptions,
|
|
1046
|
+
moduleException,
|
|
1047
|
+
totalCodeSize,
|
|
1048
|
+
});
|
|
1049
|
+
// keeping visited modules and functions, but the hints are no longer needed in this process
|
|
1050
|
+
hints.clearHints();
|
|
1051
|
+
totalCodeSize = 0;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
// sandbox global builtins
|
|
1055
|
+
patchGlobalBuiltins();
|
|
1056
|
+
// start processing
|
|
1057
|
+
main().catch(err => {
|
|
1058
|
+
logger.error(`Fatal error: ${err instanceof Error ? err.stack : err}`);
|
|
1059
|
+
process.exit(1);
|
|
1060
|
+
});
|