@coana-tech/cli 15.0.1 → 15.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ });