vitest-pool-assemblyscript 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/BINARYEN_VERSION +1 -0
  2. package/LICENSE +53 -0
  3. package/README.md +607 -0
  4. package/assembly/compare.ts +219 -0
  5. package/assembly/describe.ts +104 -0
  6. package/assembly/expect.ts +335 -0
  7. package/assembly/index.ts +14 -0
  8. package/assembly/options.ts +198 -0
  9. package/assembly/test.ts +147 -0
  10. package/assembly/tsconfig.json +6 -0
  11. package/binding.gyp +62 -0
  12. package/dist/ast-visitor-DC3SuTzs.mjs +310 -0
  13. package/dist/ast-visitor-DC3SuTzs.mjs.map +1 -0
  14. package/dist/compile-runner-8h0dBwG2.mjs +80 -0
  15. package/dist/compile-runner-8h0dBwG2.mjs.map +1 -0
  16. package/dist/compiler/transforms/strip-inline.d.mts +18 -0
  17. package/dist/compiler/transforms/strip-inline.d.mts.map +1 -0
  18. package/dist/compiler/transforms/strip-inline.mjs +38 -0
  19. package/dist/compiler/transforms/strip-inline.mjs.map +1 -0
  20. package/dist/compiler-CN6BRK_N.mjs +295 -0
  21. package/dist/compiler-CN6BRK_N.mjs.map +1 -0
  22. package/dist/config/index-v3.d.mts +111 -0
  23. package/dist/config/index-v3.d.mts.map +1 -0
  24. package/dist/config/index-v3.mjs +11 -0
  25. package/dist/config/index-v3.mjs.map +1 -0
  26. package/dist/config/index.d.mts +4 -0
  27. package/dist/config/index.mjs +8 -0
  28. package/dist/constants-CA50WBdr.mjs +130 -0
  29. package/dist/constants-CA50WBdr.mjs.map +1 -0
  30. package/dist/coverage-merge-0WqdC-dq.mjs +22 -0
  31. package/dist/coverage-merge-0WqdC-dq.mjs.map +1 -0
  32. package/dist/coverage-provider/index.d.mts +15 -0
  33. package/dist/coverage-provider/index.d.mts.map +1 -0
  34. package/dist/coverage-provider/index.mjs +535 -0
  35. package/dist/coverage-provider/index.mjs.map +1 -0
  36. package/dist/custom-provider-options-CF5C1kXb.d.mts +26 -0
  37. package/dist/custom-provider-options-CF5C1kXb.d.mts.map +1 -0
  38. package/dist/debug-IeEHsxy0.mjs +195 -0
  39. package/dist/debug-IeEHsxy0.mjs.map +1 -0
  40. package/dist/index-internal.d.mts +23 -0
  41. package/dist/index-internal.d.mts.map +1 -0
  42. package/dist/index-internal.mjs +4 -0
  43. package/dist/index-v3.d.mts +7 -0
  44. package/dist/index-v3.d.mts.map +1 -0
  45. package/dist/index-v3.mjs +206 -0
  46. package/dist/index-v3.mjs.map +1 -0
  47. package/dist/index.d.mts +3 -0
  48. package/dist/index.mjs +8 -0
  49. package/dist/load-user-imports-J9eaAW0_.mjs +801 -0
  50. package/dist/load-user-imports-J9eaAW0_.mjs.map +1 -0
  51. package/dist/pool-runner-init-CEwLyNI3.d.mts +8 -0
  52. package/dist/pool-runner-init-CEwLyNI3.d.mts.map +1 -0
  53. package/dist/pool-runner-init-d5qScS41.mjs +400 -0
  54. package/dist/pool-runner-init-d5qScS41.mjs.map +1 -0
  55. package/dist/pool-thread/compile-worker-thread.d.mts +7 -0
  56. package/dist/pool-thread/compile-worker-thread.d.mts.map +1 -0
  57. package/dist/pool-thread/compile-worker-thread.mjs +42 -0
  58. package/dist/pool-thread/compile-worker-thread.mjs.map +1 -0
  59. package/dist/pool-thread/test-worker-thread.d.mts +7 -0
  60. package/dist/pool-thread/test-worker-thread.d.mts.map +1 -0
  61. package/dist/pool-thread/test-worker-thread.mjs +39 -0
  62. package/dist/pool-thread/test-worker-thread.mjs.map +1 -0
  63. package/dist/pool-thread/v3-tinypool-thread.d.mts +7 -0
  64. package/dist/pool-thread/v3-tinypool-thread.d.mts.map +1 -0
  65. package/dist/pool-thread/v3-tinypool-thread.mjs +57 -0
  66. package/dist/pool-thread/v3-tinypool-thread.mjs.map +1 -0
  67. package/dist/resolve-config-as1w-Qyz.mjs +65 -0
  68. package/dist/resolve-config-as1w-Qyz.mjs.map +1 -0
  69. package/dist/test-runner-B2BpyPNK.mjs +142 -0
  70. package/dist/test-runner-B2BpyPNK.mjs.map +1 -0
  71. package/dist/types-8KKo9Hbf.d.mts +228 -0
  72. package/dist/types-8KKo9Hbf.d.mts.map +1 -0
  73. package/dist/vitest-file-tasks-BUwzh375.mjs +61 -0
  74. package/dist/vitest-file-tasks-BUwzh375.mjs.map +1 -0
  75. package/dist/vitest-tasks-BKS7689f.mjs +319 -0
  76. package/dist/vitest-tasks-BKS7689f.mjs.map +1 -0
  77. package/dist/worker-rpc-channel-lbhK7Qz8.mjs +25 -0
  78. package/dist/worker-rpc-channel-lbhK7Qz8.mjs.map +1 -0
  79. package/package.json +112 -0
  80. package/prebuilds/linux-x64/vitest-pool-assemblyscript.glibc.node +0 -0
  81. package/scripts/install.js +91 -0
  82. package/scripts/setup-binaryen.js +179 -0
  83. package/src/native-instrumentation/addon.cpp +788 -0
@@ -0,0 +1,788 @@
1
+ /*
2
+ * wasm-binaryen-debug Native Addon
3
+ *
4
+ * Wraps Binaryen's C++ API to extract detailed debug information from WebAssembly binaries.
5
+ * Provides expression-level debug locations that the JavaScript API doesn't expose.
6
+ */
7
+
8
+ #include <napi.h>
9
+ #include <vector>
10
+ #include <string>
11
+ #include <map>
12
+ #include <unordered_set>
13
+ #include <sstream>
14
+
15
+ // Binaryen C++ API headers
16
+ #include "wasm-binary.h"
17
+ #include "wasm-io.h"
18
+ #include "wasm-builder.h"
19
+ #include "ir/module-utils.h"
20
+ #include "ir/names.h"
21
+ #include "cfg/cfg-traversal.h"
22
+ #include "support/name.h"
23
+ #include "pass.h"
24
+
25
+ using namespace wasm;
26
+
27
+ // 32
28
+ const uint32_t BYTES_PER_COUNTER = 4;
29
+
30
+ // 1 page = 64KB / 4bytes (32bits) each = 16384 counters
31
+ const uint32_t COUNTERS_PER_PAGE = 16384;
32
+
33
+ // TODO - pass these through the call stack as params instead
34
+ // for now we don't expect them to change between different calls
35
+ // in the same thread over the same vitest run, so it's safe to use this approach
36
+ thread_local bool DEBUG = false;
37
+ thread_local std::string LOG_PREFIX = "InstNative";
38
+
39
+ struct SourceDebugLocation {
40
+ bool exists;
41
+ uint32_t fileIndex; // Debug location file index
42
+ uint32_t lineNumber; // Debug location line number
43
+ uint32_t columnNumber; // Debug location column number
44
+ };
45
+
46
+ /**
47
+ * Structure to hold expression information during AST walk
48
+ */
49
+ struct ExpressionInfo {
50
+ std::string type; // Expression type name
51
+ uint32_t fileIndex; // Debug location file index
52
+ uint32_t lineNumber; // Debug location line number
53
+ uint32_t columnNumber; // Debug location column number
54
+ bool hasDebugLocation; // Whether debug location exists
55
+ bool isBranch; // Whether this is a branch expression
56
+ uint32_t branchPaths; // Number of branch paths (if isBranch)
57
+ };
58
+
59
+ /**
60
+ * Structure to hold basic block information
61
+ */
62
+ struct BasicBlockInfo {
63
+ std::vector<size_t> expressionIndices; // Indices into the flat expression array
64
+ std::vector<size_t> branches; // Indices of blocks this block branches to
65
+ };
66
+
67
+ // Data structure to collect function info during instrumentation
68
+ struct FunctionInfo {
69
+ std::string name;
70
+ uint32_t coverageMemoryIndex;
71
+ SourceDebugLocation representativeLocation;
72
+ std::vector<ExpressionInfo> expressions;
73
+ std::vector<BasicBlockInfo> blocks;
74
+ };
75
+
76
+ /**
77
+ * Custom content structure for CFGWalker
78
+ */
79
+ struct BlockContent {
80
+ std::vector<Expression*> expressions;
81
+ };
82
+
83
+ /**
84
+ * Walker to extract expression and basic block information using CFGWalker
85
+ *
86
+ * This walker traverses the AST in a single pass to collect:
87
+ * 1. All expressions with their debug locations and types
88
+ * 2. Basic block groupings with branch edges
89
+ */
90
+ struct DebugInfoWalker : public WalkerPass<CFGWalker<DebugInfoWalker, UnifiedExpressionVisitor<DebugInfoWalker>, BlockContent>> {
91
+ Module* module;
92
+
93
+ // Results for current function
94
+ std::vector<ExpressionInfo> expressions;
95
+ std::vector<BasicBlockInfo> blocks;
96
+
97
+ // Map from BasicBlock pointer to block index for building branches
98
+ std::map<BasicBlock*, size_t> blockIndexMap;
99
+
100
+ explicit DebugInfoWalker(Module* m) : module(m) {}
101
+
102
+ /**
103
+ * Called for each expression during CFG walk
104
+ * Collects expression info and adds to current basic block
105
+ */
106
+ void visitExpression(Expression* curr) {
107
+ // skip collecting expressions if:
108
+ // - Not currently inside a basicBlock (`currBasicBlock` provided by CFGWalker)
109
+ // - expression is a Block (Blocks are only containers and have no debug locations)
110
+ if (!currBasicBlock || curr->is<Block>()) {
111
+ return;
112
+ }
113
+
114
+ // Get debug location from function's debugLocations map
115
+ Function* func = getFunction();
116
+ ExpressionInfo info;
117
+ info.hasDebugLocation = false;
118
+
119
+ // Check debugLocations map
120
+ auto it = func->debugLocations.find(curr);
121
+ if (it != func->debugLocations.end() && it->second.has_value()) {
122
+ const auto& loc = it->second.value();
123
+ info.fileIndex = loc.fileIndex;
124
+ info.lineNumber = loc.lineNumber;
125
+ info.columnNumber = loc.columnNumber;
126
+ info.hasDebugLocation = true;
127
+ }
128
+
129
+ // skip expressions without debug locations
130
+ // TODO - determine if this will cause problems in branch coverage
131
+ if (!info.hasDebugLocation) {
132
+ return;
133
+ }
134
+
135
+ info.type = getExpressionName(curr); // Expression type string
136
+
137
+ // Determine if this is a branch expression and count paths
138
+ info.isBranch = false;
139
+ info.branchPaths = 0;
140
+
141
+ // TODO - determine if we're missing any branch types (SIMDTernary?)
142
+ if (curr->is<If>()) {
143
+ info.isBranch = true;
144
+ auto* ifExpr = curr->cast<If>();
145
+ info.branchPaths = ifExpr->ifFalse ? 2 : 1; // If/else = 2, if only = 1
146
+ } else if (curr->is<Break>()) {
147
+ info.isBranch = true;
148
+ info.branchPaths = 2; // Branch taken or not (conditional break)
149
+ } else if (curr->is<Select>()) {
150
+ info.isBranch = true;
151
+ info.branchPaths = 2; // True or false condition
152
+ } else if (curr->is<Switch>()) {
153
+ info.isBranch = true;
154
+ auto* switchExpr = curr->cast<Switch>();
155
+ info.branchPaths = switchExpr->targets.size() + 1; // N targets + default
156
+ }
157
+
158
+ // Add to flat expressions array
159
+ expressions.push_back(info);
160
+
161
+ // Add expression to current basic block's content
162
+ currBasicBlock->contents.expressions.push_back(curr);
163
+ }
164
+
165
+ /**
166
+ * Called for each function
167
+ * Walks the function and collects expression + basic block data
168
+ */
169
+ void doWalkFunction(Function* func) {
170
+ // Reset state for this function
171
+ expressions.clear();
172
+ blocks.clear();
173
+ blockIndexMap.clear();
174
+
175
+ // Walk the function using CFGWalker
176
+ CFGWalker<DebugInfoWalker, UnifiedExpressionVisitor<DebugInfoWalker>, BlockContent>::doWalkFunction(func);
177
+
178
+ // After walk, build out basic block info with expression indices.
179
+ // `basicBlocks` provided by CFGWalker, now populated after the function walk
180
+ size_t exprIndex = 0;
181
+ for (auto& bb : basicBlocks) {
182
+ BasicBlockInfo blockInfo;
183
+
184
+ // Store the index for this block
185
+ blockIndexMap[bb.get()] = blocks.size();
186
+
187
+ // Record expression indices for this block
188
+ size_t expressionCount = bb->contents.expressions.size();
189
+ for (size_t i = 0; i < expressionCount; i++) {
190
+ blockInfo.expressionIndices.push_back(exprIndex++);
191
+ }
192
+
193
+ blocks.push_back(blockInfo);
194
+ }
195
+
196
+ // Now build branch edges between blocks
197
+ for (size_t i = 0; i < basicBlocks.size(); i++) {
198
+ auto& bb = basicBlocks[i];
199
+ for (auto* outBlock : bb->out) {
200
+ auto it = blockIndexMap.find(outBlock);
201
+ if (it != blockIndexMap.end()) {
202
+ blocks[i].branches.push_back(it->second);
203
+ }
204
+ }
205
+ }
206
+ }
207
+ };
208
+
209
+ bool startsWith(const std::string& str, const std::string& prefix) {
210
+ return str.compare(0, prefix.length(), prefix) == 0;
211
+ }
212
+
213
+ /**
214
+ * Check if a function should be instrumented for coverage
215
+ */
216
+ bool shouldInstrumentFunction(Function* func, std::string& excludedLibraryFilePrefix) {
217
+ const std::string& name = func->name.toString();
218
+
219
+ // Skip functions without a body
220
+ if (!func->body) {
221
+ if (DEBUG) {
222
+ std::cout << LOG_PREFIX << " - Skip Reason: Empty Function Body" << std::endl;
223
+ }
224
+ return false;
225
+ }
226
+
227
+ // Skip if this is an import (has non-empty module)
228
+ if (func->module.size() > 0) {
229
+ if (DEBUG) {
230
+ std::cout << LOG_PREFIX << " - Skip Reason: Imported from \"" << func->module.toString() << "\"" << std::endl;
231
+ }
232
+ return false;
233
+ }
234
+
235
+ // Skip library functions
236
+ if (excludedLibraryFilePrefix.length() > 0 && startsWith(name, excludedLibraryFilePrefix)) {
237
+ if (DEBUG) {
238
+ std::cout << LOG_PREFIX << " - Skip Reason: Library file" << std::endl;
239
+ }
240
+ return false;
241
+ }
242
+
243
+ // Compiler-generated entry point
244
+ if (name.compare("~start") == 0) {
245
+ if (DEBUG) {
246
+ std::cout << LOG_PREFIX << " - Skip Reason: Module entry point" << std::endl;
247
+ }
248
+ return false;
249
+ }
250
+
251
+ return true;
252
+ }
253
+
254
+ /**
255
+ * Find representative expression within a function's Block-type body (Return preferred, then first non-Const)
256
+ */
257
+ SourceDebugLocation getRepresentativeLocationInBlockBody(
258
+ Block* blockBody,
259
+ const std::unordered_map<wasm::Expression*, std::optional<wasm::Function::DebugLocation>> debugLocations
260
+ ) {
261
+ SourceDebugLocation repLoc = { exists: false, fileIndex: 0, lineNumber: 0, columnNumber: 0 };
262
+
263
+ if (DEBUG) {
264
+ std::cout << LOG_PREFIX << " - Checking func Block body: " << blockBody->list.size() << " body expressions" << std::endl;
265
+ }
266
+
267
+ for (size_t i = 0; i < blockBody->list.size(); i++) {
268
+ Expression* exprInBlockBody = blockBody->list[i];
269
+
270
+ if (exprInBlockBody) {
271
+ auto it = debugLocations.find(exprInBlockBody);
272
+ if (it != debugLocations.end() && it->second.has_value()) {
273
+ const auto& loc = it->second.value();
274
+
275
+ repLoc.exists = true;
276
+ repLoc.fileIndex = loc.fileIndex;
277
+ repLoc.lineNumber = loc.lineNumber;
278
+ repLoc.columnNumber = loc.columnNumber;
279
+
280
+ if (DEBUG) {
281
+ std::cout << LOG_PREFIX << " - Block body expr [" << i << "] (" << getExpressionName(exprInBlockBody) << ")="
282
+ << loc.fileIndex << ":" << loc.lineNumber << ":" << loc.columnNumber << " - break" << std::endl;
283
+ }
284
+
285
+ break;
286
+
287
+ } else if (DEBUG) {
288
+ std::cout << LOG_PREFIX << " - Block body expr [" << i << "] (" << getExpressionName(exprInBlockBody) << ") - No location" << std::endl;
289
+ }
290
+ } else if (DEBUG) {
291
+ std::cout << LOG_PREFIX << " - WARNING: Block body expr [" << i << "] - EMPTY" << std::endl;
292
+ }
293
+ }
294
+
295
+ return repLoc;
296
+ }
297
+
298
+ SourceDebugLocation getRepresentativeLocation(Function* func) {
299
+ SourceDebugLocation repLoc = { exists: false, fileIndex: 0, lineNumber: 0, columnNumber: 0 };
300
+
301
+ // Get body expression debug location
302
+ Expression* body = func->body;
303
+
304
+ if (!body) {
305
+ if (DEBUG) {
306
+ std::cout << LOG_PREFIX << " - Function has no body expression - No debug locations available to check" << std::endl;
307
+ }
308
+ return repLoc;
309
+ }
310
+
311
+ const std::string bodyType = getExpressionName(body);
312
+
313
+ if (body->is<Load>() || body->is<Store>()) {
314
+ // Load/Store body:
315
+ // - Compiler-generated functions with no expressions with locations
316
+ // - LOAD: Compiler-generated class member getters (field value getters, function member getters)
317
+ // - STORE: Compiler-generated class member value setters (field value setters)
318
+ //
319
+ // Note: compiler-generated class member function setters use a Block body also,
320
+ // but their expressions (Store+Call) have no locations
321
+ if (DEBUG) {
322
+ std::cout << LOG_PREFIX << " - Compiler-generated accessor function (body=" << bodyType << ") - No location" << std::endl;
323
+ }
324
+ return repLoc;
325
+ } else if (body->is<Block>()) {
326
+ // Block body:
327
+ // - Block expressions are only containers and have no source locations of their own
328
+ // - Examine expressions within the block body to find location, if one exists
329
+ if (DEBUG) {
330
+ std::cout << LOG_PREFIX << " - Checking function Block body expression list" << std::endl;
331
+ }
332
+
333
+ repLoc = getRepresentativeLocationInBlockBody(body->cast<Block>(), func->debugLocations);
334
+ }
335
+
336
+ // use body expression's debug location if available
337
+ auto it = func->debugLocations.find(body);
338
+ if (it != func->debugLocations.end() && it->second.has_value()) {
339
+ const auto& loc = it->second.value();
340
+ repLoc.exists = true;
341
+ repLoc.fileIndex = loc.fileIndex;
342
+ repLoc.lineNumber = loc.lineNumber;
343
+ repLoc.columnNumber = loc.columnNumber;
344
+
345
+ if (DEBUG) {
346
+ std::cout << LOG_PREFIX << " - Using function body (" << bodyType << ")="
347
+ << repLoc.fileIndex << ":" << repLoc.lineNumber << ":" << repLoc.columnNumber << std::endl;
348
+ }
349
+ }
350
+
351
+ if (!repLoc.exists && DEBUG) {
352
+ std::cout << LOG_PREFIX << " - Warning: Location expected on function body (" << bodyType << ") - No location found" << std::endl;
353
+ }
354
+
355
+ return repLoc;
356
+ }
357
+
358
+ /**
359
+ * Instrument WASM binary for coverage and regenerate source map
360
+ *
361
+ * This function:
362
+ * 1. Reads WASM binary with source map
363
+ * 2. Adds __coverage_memory import (multi-memory for coverage counters)
364
+ * 3. Instruments each user function with coverage counter increment
365
+ * 4. Extracts debug information
366
+ * 5. Writes instrumented binary with regenerated source map
367
+ *
368
+ * @param wasmBuffer - Node.js Buffer containing WASM binary
369
+ * @param sourceMapBuffer - Node.js Buffer containing source map JSON
370
+ * @returns Object with { instrumentedWasm, sourceMap, debugInfo }
371
+ */
372
+ Napi::Object InstrumentForCoverage(const Napi::CallbackInfo& info) {
373
+ Napi::Env env = info.Env();
374
+
375
+ // Validate arguments
376
+ if (info.Length() < 3) {
377
+ Napi::TypeError::New(env, "Expected 3 arguments: wasmBuffer, sourceMapBuffer, instrumentationOptions")
378
+ .ThrowAsJavaScriptException();
379
+ return Napi::Object::New(env);
380
+ }
381
+
382
+ if (!info[0].IsBuffer()) {
383
+ Napi::TypeError::New(env, "Argument 0 (wasmBuffer) must be a Buffer (WASM binary)")
384
+ .ThrowAsJavaScriptException();
385
+ return Napi::Object::New(env);
386
+ }
387
+
388
+ if (!info[1].IsBuffer()) {
389
+ Napi::TypeError::New(env, "Argument 1 (sourceMapBuffer) must be a Buffer (source map)")
390
+ .ThrowAsJavaScriptException();
391
+ return Napi::Object::New(env);
392
+ }
393
+
394
+ if (!info[2].IsObject()) {
395
+ Napi::TypeError::New(env, "Argument 2 (instrumentationOptions) must be supplied as an object")
396
+ .ThrowAsJavaScriptException();
397
+ return Napi::Object::New(env);
398
+ }
399
+
400
+ try {
401
+ // Extract buffer data
402
+ Napi::Buffer<char> wasmBuf = info[0].As<Napi::Buffer<char>>();
403
+ Napi::Buffer<char> sourceMapBuf = info[1].As<Napi::Buffer<char>>();
404
+
405
+ // Extract options
406
+ const Napi::Object options = info[2].As<Napi::Object>();
407
+
408
+ // Extracted options
409
+ std::unordered_set<std::string> excludedFiles;
410
+ std::string excludedLibraryFilePrefix;
411
+ // 1 page = 64KB / 4bytes (32bits) each = 16384 counters
412
+ uint32_t coverageMemoryPagesMin = 1;
413
+ // 4 pages = 256KB / 4bytes (32bits) each = 65536 counters
414
+ uint32_t coverageMemoryPagesMax = 4;
415
+ uint32_t maxCounters = coverageMemoryPagesMax * COUNTERS_PER_PAGE;
416
+
417
+ if (options.Has("logPrefix")) {
418
+ Napi::Value logPrefixProp = options.Get("logPrefix");
419
+ if (logPrefixProp.IsString()) {
420
+ LOG_PREFIX = logPrefixProp.As<Napi::String>().Utf8Value();
421
+ }
422
+ }
423
+
424
+ if (options.Has("debug")) {
425
+ Napi::Value debugProperty = options.Get("debug");
426
+ if (debugProperty.IsBoolean()) {
427
+ DEBUG = debugProperty.As<Napi::Boolean>().Value();
428
+
429
+ if (DEBUG) {
430
+ std::cout << LOG_PREFIX << " - OPTIONS - DEBUG enabled" << std::endl;
431
+ }
432
+ }
433
+ }
434
+
435
+ if (options.Has("excludedFiles")) {
436
+ Napi::Value excludedFilesProperty = options.Get("excludedFiles");
437
+ if (excludedFilesProperty.IsArray()) {
438
+ const Napi::Array filesArray = excludedFilesProperty.As<Napi::Array>();
439
+
440
+ const uint32_t count = filesArray.Length();
441
+ if (DEBUG && count > 0) {
442
+ std::cout << LOG_PREFIX << " - OPTIONS - " << count << " Excluded Files:" << std::endl;
443
+ } else if (DEBUG) {
444
+ std::cout << LOG_PREFIX << " - 0 Excluded Files" << std::endl;
445
+ }
446
+
447
+ for (size_t i = 0; i < count; i++) {
448
+ Napi::Value fileItem = filesArray[i];
449
+ if (fileItem.IsString()) {
450
+ const std::string file = fileItem.As<Napi::String>().Utf8Value();
451
+ excludedFiles.insert(file);
452
+ if (DEBUG) {
453
+ std::cout << LOG_PREFIX << " - [" << i << "] \"" << file << "\"" << std::endl;
454
+ }
455
+ }
456
+ }
457
+ }
458
+ }
459
+
460
+ if (options.Has("excludedLibraryFilePrefix")) {
461
+ Napi::Value libraryFilePrefixProperty = options.Get("excludedLibraryFilePrefix");
462
+ if (libraryFilePrefixProperty.IsString()) {
463
+ excludedLibraryFilePrefix = libraryFilePrefixProperty.As<Napi::String>().Utf8Value();
464
+
465
+ if (DEBUG) {
466
+ std::cout << LOG_PREFIX << " - OPTIONS - Excluded Library File Prefix: \"" << excludedLibraryFilePrefix << "\"" << std::endl;
467
+ }
468
+ }
469
+ }
470
+
471
+ if (options.Has("coverageMemoryPagesMin")) {
472
+ Napi::Value coverageMinProperty = options.Get("coverageMemoryPagesMin");
473
+ if (coverageMinProperty.IsNumber()) {
474
+ coverageMemoryPagesMin = coverageMinProperty.As<Napi::Number>().Int32Value();
475
+
476
+ if (DEBUG) {
477
+ const uint32_t minCounters = coverageMemoryPagesMin * COUNTERS_PER_PAGE;
478
+ std::cout << LOG_PREFIX << " - OPTIONS - Coverage Memory Pages MIN: " << coverageMemoryPagesMin
479
+ << " (" << minCounters << " counters)" << std::endl;
480
+ }
481
+ }
482
+ }
483
+
484
+ if (options.Has("coverageMemoryPagesMax")) {
485
+ Napi::Value coverageMaxProperty = options.Get("coverageMemoryPagesMax");
486
+ if (coverageMaxProperty.IsNumber()) {
487
+ coverageMemoryPagesMax = coverageMaxProperty.As<Napi::Number>().Int32Value();
488
+ maxCounters = coverageMemoryPagesMax * COUNTERS_PER_PAGE;
489
+
490
+ if (DEBUG) {
491
+ std::cout << LOG_PREFIX << " - OPTIONS - Coverage Memory Pages MAX: " << coverageMemoryPagesMax
492
+ << " (" << maxCounters << " counters)" << std::endl;
493
+ }
494
+ }
495
+ }
496
+
497
+ std::vector<char> wasmData(wasmBuf.Data(), wasmBuf.Data() + wasmBuf.Length());
498
+ std::vector<char> sourceMapData(sourceMapBuf.Data(), sourceMapBuf.Data() + sourceMapBuf.Length());
499
+
500
+ // Parse WASM binary with source map
501
+ Module module;
502
+ WasmBinaryReader reader(module, FeatureSet::All, wasmData, sourceMapData);
503
+ reader.setDebugInfo(true);
504
+ reader.read();
505
+
506
+ if (DEBUG) {
507
+ std::cout << LOG_PREFIX << " - Read binary module with " << module.functions.size() << " functions" << std::endl;
508
+ std::cout << LOG_PREFIX << " - Debug source files: " << module.debugInfoFileNames.size() << std::endl;
509
+ for (size_t i = 0; i < module.debugInfoFileNames.size(); i++) {
510
+ std::cout << LOG_PREFIX << " - [" << i << "] " << module.debugInfoFileNames[i] << std::endl;
511
+ }
512
+ }
513
+
514
+ // Instrument functions and collect debug info
515
+ Builder builder(module);
516
+ uint32_t coverageIndex = 0;
517
+ std::vector<FunctionInfo> instrumentedFunctions;
518
+
519
+ // Enable multi-memory feature for coverage memory
520
+ module.features.setMultiMemory(true);
521
+
522
+ // Add __coverage_memory import
523
+ // This is a secondary memory used to store coverage counters
524
+ Name coverageMemoryName("__coverage_memory");
525
+ auto coverageMemory = Builder::makeMemory(coverageMemoryName);
526
+ coverageMemory->module = "__as_pool_env__";
527
+ coverageMemory->base = "__coverage_memory";
528
+ coverageMemory->initial = coverageMemoryPagesMin;
529
+ coverageMemory->max = coverageMemoryPagesMax;
530
+ coverageMemory->shared = false;
531
+ module.addMemory(std::move(coverageMemory));
532
+
533
+ // Create walker for debug info extraction
534
+ DebugInfoWalker walker(&module);
535
+
536
+ ModuleUtils::iterDefinedFunctions(module, [&](Function* func) {
537
+ std::string funcName = func->name.toString();
538
+
539
+ if (coverageIndex >= maxCounters) {
540
+ if (DEBUG) {
541
+ std::cout << LOG_PREFIX << " - ERROR: Processing function: \"" << funcName << "\""
542
+ << " Further instrumentation would exceed max covergare memory size" << std::endl;
543
+ }
544
+ return;
545
+ }
546
+
547
+ if (DEBUG) {
548
+ std::cout << LOG_PREFIX << " - Processing function: \"" << funcName << "\"" << std::endl;
549
+ }
550
+
551
+ // Check if this function should be instrumented
552
+ if (!shouldInstrumentFunction(func, excludedLibraryFilePrefix)) {
553
+ if (DEBUG) {
554
+ std::cout << LOG_PREFIX << " - SKIP function (quick filtered): \"" << funcName << "\"" << std::endl;
555
+ }
556
+ return;
557
+ }
558
+
559
+ // Walk function to collect expressions and basic blocks
560
+ walker.walkFunctionInModule(func, &module);
561
+
562
+ if (DEBUG) {
563
+ std::cout << LOG_PREFIX << " - CFG Walked function - expressions with locations: " << walker.expressions.size() << std::endl;
564
+ }
565
+
566
+ const SourceDebugLocation representativeLocation = getRepresentativeLocation(func);
567
+
568
+ // skip function if it has no representative location
569
+ if (!representativeLocation.exists) {
570
+ if (DEBUG) {
571
+ std::cout << LOG_PREFIX << " - SKIP function (No Representative Location, body=" << getExpressionName(func->body) << "): "
572
+ << "\"" << funcName << "\"" << std::endl;
573
+ }
574
+ return;
575
+ }
576
+
577
+ // Skip function if located within excluded file
578
+ const std::string functionDebugFilePath = module.debugInfoFileNames[representativeLocation.fileIndex];
579
+ if (excludedFiles.find(functionDebugFilePath) != excludedFiles.end()) {
580
+ if (DEBUG) {
581
+ std::cout << LOG_PREFIX << " - SKIP function (excluded location file [" << representativeLocation.fileIndex << "] \""
582
+ << functionDebugFilePath <<"\"): \"" << funcName << "\"" << std::endl;
583
+ }
584
+ return;
585
+ }
586
+
587
+ if (DEBUG) {
588
+ std::cout << LOG_PREFIX << " - Selected reprLoc=" << representativeLocation.fileIndex << ":" << representativeLocation.lineNumber
589
+ << ":" << representativeLocation.columnNumber << " | Now instrumenting with coverageMemoryIndex [" << coverageIndex << "]"
590
+ << " | " << std::endl;
591
+ }
592
+
593
+ // Store function info for later output
594
+ FunctionInfo funcInfo;
595
+ funcInfo.name = funcName;
596
+ funcInfo.representativeLocation = representativeLocation;
597
+ funcInfo.coverageMemoryIndex = coverageIndex;
598
+ funcInfo.expressions = walker.expressions;
599
+ funcInfo.blocks = walker.blocks;
600
+
601
+ // add to list
602
+ instrumentedFunctions.push_back(funcInfo);
603
+
604
+ // Coverage instrumentation:
605
+ // counter = i32.load(addr, __coverage_memory)
606
+ // incremented = counter + 1
607
+ // i32.store(addr, incremented, __coverage_memory)
608
+
609
+ const uint32_t counterAddressVal = coverageIndex * BYTES_PER_COUNTER;
610
+ Expression* counterAddress = builder.makeConstantExpression(Literal(counterAddressVal));
611
+
612
+ // Load current counter value
613
+ Expression* counterValue = builder.makeLoad(
614
+ BYTES_PER_COUNTER, // bytes - size
615
+ false, // signed - false, treat as unsigned (and no extension needed anyway)
616
+ 0, // offset - none, we already calculate the address based on data size
617
+ BYTES_PER_COUNTER, // align - we should always be aligned
618
+ counterAddress, // address
619
+ Type::i32,
620
+ coverageMemoryName
621
+ );
622
+
623
+ // Increment counter
624
+ Expression* incrementedCounter = builder.makeBinary(
625
+ AddInt32,
626
+ counterValue,
627
+ builder.makeConst(1)
628
+ );
629
+
630
+ Expression* storeCounter = builder.makeStore(
631
+ BYTES_PER_COUNTER, // bytes
632
+ 0, // offset
633
+ BYTES_PER_COUNTER, // align hint
634
+ counterAddress, // address
635
+ incrementedCounter, // value
636
+ Type::i32,
637
+ coverageMemoryName
638
+ );
639
+
640
+ // Prepend instrumentation to function body
641
+ func->body = builder.makeSequence(storeCounter, func->body, func->body->type);
642
+
643
+ if (DEBUG) {
644
+ std::cout << LOG_PREFIX << " - INSTRUMENTED \"" << funcName << "\" | coverageMemoryIndex [" << coverageIndex << "]"
645
+ << " | reprLoc=" << representativeLocation.fileIndex << ":" << representativeLocation.lineNumber
646
+ << ":" << representativeLocation.columnNumber << std::endl;
647
+ }
648
+
649
+ coverageIndex++;
650
+ });
651
+
652
+ if (DEBUG) {
653
+ std::cout << LOG_PREFIX << " - Instrumentation complete: " << coverageIndex << " functions instrumented" << std::endl;
654
+ }
655
+
656
+ // Write instrumented module with source map regeneration
657
+ BufferWithRandomAccess outputBuffer;
658
+ PassOptions passOptions;
659
+ WasmBinaryWriter writer(&module, outputBuffer, passOptions);
660
+ writer.setNamesSection(true);
661
+
662
+ // Set up source map output stream
663
+ std::ostringstream sourceMapStream;
664
+ writer.setSourceMap(&sourceMapStream, "output.wasm");
665
+
666
+ writer.write();
667
+
668
+ // Build result object
669
+ Napi::Object result = Napi::Object::New(env);
670
+
671
+ // Convert instrumented binary to Buffer
672
+ Napi::Buffer<char> instrumentedWasm = Napi::Buffer<char>::Copy(
673
+ env,
674
+ reinterpret_cast<const char*>(outputBuffer.data()),
675
+ outputBuffer.size()
676
+ );
677
+ result.Set("instrumentedWasm", instrumentedWasm);
678
+
679
+ // Convert source map to string
680
+ std::string sourceMapStr = sourceMapStream.str();
681
+ result.Set("sourceMap", Napi::String::New(env, sourceMapStr));
682
+
683
+ // Build debug info object (similar structure to ExtractDebugInfo output)
684
+ Napi::Object debugInfo = Napi::Object::New(env);
685
+
686
+ // Add debug source files
687
+ Napi::Array debugSourceFiles = Napi::Array::New(env, module.debugInfoFileNames.size());
688
+ for (size_t i = 0; i < module.debugInfoFileNames.size(); i++) {
689
+ debugSourceFiles[i] = Napi::String::New(env, module.debugInfoFileNames[i]);
690
+ }
691
+ debugInfo.Set("debugSourceFiles", debugSourceFiles);
692
+
693
+ // Add function information
694
+ Napi::Array functions = Napi::Array::New(env, instrumentedFunctions.size());
695
+
696
+ for (size_t i = 0; i < instrumentedFunctions.size(); i++) {
697
+ const auto& funcInfo = instrumentedFunctions[i];
698
+ Napi::Object funcObj = Napi::Object::New(env);
699
+
700
+ funcObj.Set("name", Napi::String::New(env, funcInfo.name));
701
+ funcObj.Set("wasmIndex", Napi::Number::New(env, i));
702
+ funcObj.Set("coverageMemoryIndex", Napi::Number::New(env, funcInfo.coverageMemoryIndex));
703
+
704
+ Napi::Object reprLoc = Napi::Object::New(env);
705
+ reprLoc.Set("fileIndex", Napi::Number::New(env, funcInfo.representativeLocation.fileIndex));
706
+ reprLoc.Set("line", Napi::Number::New(env, funcInfo.representativeLocation.lineNumber));
707
+ reprLoc.Set("column", Napi::Number::New(env, funcInfo.representativeLocation.columnNumber));
708
+ funcObj.Set("representativeLocation", reprLoc);
709
+
710
+ // Add expressions array
711
+ Napi::Array expressions = Napi::Array::New(env, funcInfo.expressions.size());
712
+ for (size_t j = 0; j < funcInfo.expressions.size(); j++) {
713
+ const auto& expr = funcInfo.expressions[j];
714
+ Napi::Object exprObj = Napi::Object::New(env);
715
+
716
+ exprObj.Set("type", Napi::String::New(env, expr.type));
717
+ exprObj.Set("isBranch", Napi::Boolean::New(env, expr.isBranch));
718
+ if (expr.isBranch) {
719
+ exprObj.Set("branchPaths", Napi::Number::New(env, expr.branchPaths));
720
+ }
721
+
722
+ if (expr.hasDebugLocation) {
723
+ Napi::Object location = Napi::Object::New(env);
724
+ location.Set("fileIndex", Napi::Number::New(env, expr.fileIndex));
725
+ location.Set("line", Napi::Number::New(env, expr.lineNumber));
726
+ location.Set("column", Napi::Number::New(env, expr.columnNumber));
727
+ exprObj.Set("location", location);
728
+ }
729
+
730
+ expressions[j] = exprObj;
731
+ }
732
+ funcObj.Set("expressions", expressions);
733
+
734
+ // Add basic blocks array
735
+ Napi::Array basicBlocks = Napi::Array::New(env, funcInfo.blocks.size());
736
+ for (size_t j = 0; j < funcInfo.blocks.size(); j++) {
737
+ const auto& block = funcInfo.blocks[j];
738
+ Napi::Object blockObj = Napi::Object::New(env);
739
+
740
+ blockObj.Set("index", Napi::Number::New(env, j));
741
+
742
+ Napi::Array exprIndices = Napi::Array::New(env, block.expressionIndices.size());
743
+ for (size_t k = 0; k < block.expressionIndices.size(); k++) {
744
+ exprIndices[k] = Napi::Number::New(env, block.expressionIndices[k]);
745
+ }
746
+ blockObj.Set("expressionIndices", exprIndices);
747
+
748
+ Napi::Array branches = Napi::Array::New(env, block.branches.size());
749
+ for (size_t k = 0; k < block.branches.size(); k++) {
750
+ Napi::Object branchObj = Napi::Object::New(env);
751
+ branchObj.Set("targetBlockIndex", Napi::Number::New(env, block.branches[k]));
752
+ branches[k] = branchObj;
753
+ }
754
+ blockObj.Set("branches", branches);
755
+
756
+ basicBlocks[j] = blockObj;
757
+ }
758
+ funcObj.Set("basicBlocks", basicBlocks);
759
+
760
+ functions[i] = funcObj;
761
+ }
762
+
763
+ debugInfo.Set("functions", functions);
764
+
765
+ result.Set("debugInfo", debugInfo);
766
+
767
+ return result;
768
+
769
+ } catch (const std::exception& e) {
770
+ Napi::Error::New(env, std::string("Failed to instrument for coverage: ") + e.what())
771
+ .ThrowAsJavaScriptException();
772
+ return Napi::Object::New(env);
773
+ } catch (...) {
774
+ Napi::Error::New(env, "Failed to instrument for coverage: Unknown error")
775
+ .ThrowAsJavaScriptException();
776
+ return Napi::Object::New(env);
777
+ }
778
+ }
779
+
780
+ /**
781
+ * Initialize the addon
782
+ */
783
+ Napi::Object Init(Napi::Env env, Napi::Object exports) {
784
+ exports.Set("instrumentForCoverage", Napi::Function::New(env, InstrumentForCoverage));
785
+ return exports;
786
+ }
787
+
788
+ NODE_API_MODULE(wasm_binaryen_debug, Init);