space-data-module-sdk 0.8.19 → 0.8.21

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 (67) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -2
  3. package/docs/assembly.js +190 -0
  4. package/docs/browser-wasmedge-isomorphic.html +6 -1
  5. package/docs/byo-wasm-quickstart.html +6 -1
  6. package/docs/conformance.html +6 -1
  7. package/docs/events-abi.html +6 -1
  8. package/docs/families/analytics.html +7 -2
  9. package/docs/families/attitude.html +7 -2
  10. package/docs/families/behavior.html +7 -2
  11. package/docs/families/breakup.html +7 -2
  12. package/docs/families/conjunction.html +7 -2
  13. package/docs/families/data-source.html +7 -2
  14. package/docs/families/effects.html +7 -2
  15. package/docs/families/environment.html +7 -2
  16. package/docs/families/estimation.html +7 -2
  17. package/docs/families/gnc.html +7 -2
  18. package/docs/families/maneuver.html +7 -2
  19. package/docs/families/obstruction.html +7 -2
  20. package/docs/families/propagator.html +7 -2
  21. package/docs/families/propulsion.html +7 -2
  22. package/docs/families/reentry.html +7 -2
  23. package/docs/families/rf.html +7 -2
  24. package/docs/families/scheduler.html +7 -2
  25. package/docs/families/sensor.html +7 -2
  26. package/docs/families/signature.html +7 -2
  27. package/docs/flatsql-host-contract.html +7 -2
  28. package/docs/flatsql-host-contract.md +2 -2
  29. package/docs/flatsql-streaming-standard.html +6 -1
  30. package/docs/gpu-module-abi.html +6 -1
  31. package/docs/index.html +21 -14
  32. package/docs/isomorphic-pthreads.html +31 -2
  33. package/docs/isomorphic-pthreads.md +104 -1
  34. package/docs/isomorphic-sdn-runtime-plan.html +6 -1
  35. package/docs/language-runtime-matrix.html +6 -1
  36. package/docs/llms.txt +1 -1
  37. package/docs/module-bundle-runtime-plan.html +6 -1
  38. package/docs/module-publication-standard.html +6 -1
  39. package/docs/node-red-default-node-parity.html +6 -1
  40. package/docs/protect-and-sign.html +6 -1
  41. package/docs/protocol-installation.html +6 -1
  42. package/docs/publication-submission.html +6 -1
  43. package/docs/secrets-capability.html +6 -1
  44. package/docs/styles.css +32 -0
  45. package/docs/testing-harness.html +6 -1
  46. package/docs/tri-runtime-parity-gate.html +6 -1
  47. package/docs/tri-runtime-parity.html +6 -1
  48. package/package.json +1 -1
  49. package/src/compiler/invokeGlue.js +38 -0
  50. package/src/host/browserModuleHarness.js +58 -35
  51. package/src/host/isomorphicLoader.js +21 -6
  52. package/src/host/wasiShim.js +57 -13
  53. package/src/host/wasiThreadBrowserWorker.mjs +4 -0
  54. package/src/host/wasiThreadHost.js +19 -3
  55. package/src/host/wasiThreadWorker.mjs +25 -2
  56. package/src/host/wasiThreadWorkerRuntime.js +2 -1
  57. package/src/index.d.ts +2 -0
  58. package/src/runtime/constants.js +6 -0
  59. package/src/testing/browserModuleHarness.js +2 -0
  60. package/src/testing/buildWasmEdgeRunner.js +147 -4
  61. package/src/testing/docker/wasmedge-parity.Dockerfile +23 -1
  62. package/src/testing/isomorphicHarness.js +5 -0
  63. package/src/testing/native/wasmedge-0.16.4-atomic-wait.patch +111 -0
  64. package/src/testing/native/wasmedge_wasi_threads_runner.c +353 -0
  65. package/src/testing/parityBrowserRunner.js +12 -4
  66. package/src/testing/parityHarness.js +17 -3
  67. package/src/testing/parityLanes.js +48 -5
@@ -0,0 +1,353 @@
1
+ // WASI preview1 command host for the SDK parity lanes. WasmEdge 0.16.4's
2
+ // CLI enables atomics but does not implement wasi.thread-spawn. Like the SDN
3
+ // Go host, use a fresh instance per worker, shared memory, and ONE executor:
4
+ // atomic.wait/notify waiters belong to the executor in this runtime revision.
5
+ #define _POSIX_C_SOURCE 200809L
6
+ #include <wasmedge/wasmedge.h>
7
+ #include <pthread.h>
8
+ #include <stdbool.h>
9
+ #include <stdint.h>
10
+ #include <stdio.h>
11
+ #include <stdlib.h>
12
+ #include <string.h>
13
+ #include <time.h>
14
+
15
+ #define MAX_WORKERS 32
16
+
17
+ typedef struct {
18
+ WasmEdge_ModuleInstanceContext *instance;
19
+ WasmEdge_Async *async;
20
+ } Worker;
21
+
22
+ typedef struct {
23
+ WasmEdge_ExecutorContext *executor;
24
+ WasmEdge_StoreContext *store;
25
+ WasmEdge_ASTModuleContext *ast;
26
+ pthread_mutex_t mutex;
27
+ Worker workers[MAX_WORKERS];
28
+ int32_t next_tid;
29
+ bool closed;
30
+ bool failed;
31
+ bool exited;
32
+ } Threads;
33
+
34
+ static WasmEdge_String name(const char *s) {
35
+ return WasmEdge_StringWrap(s, (uint32_t)strlen(s));
36
+ }
37
+
38
+ static uint64_t monotonic_ms(void) {
39
+ struct timespec now;
40
+ clock_gettime(CLOCK_MONOTONIC, &now);
41
+ return (uint64_t)now.tv_sec * 1000 + (uint64_t)now.tv_nsec / 1000000;
42
+ }
43
+
44
+ static bool wait_until(WasmEdge_Async *call, uint64_t deadline) {
45
+ uint64_t now = monotonic_ms();
46
+ return WasmEdge_AsyncWaitFor(call, now < deadline ? deadline - now : 0);
47
+ }
48
+
49
+ static void require_result(const char *step, WasmEdge_Result result) {
50
+ if (!WasmEdge_ResultOK(result)) {
51
+ fprintf(stderr, "wasm trap: %s: %s\n", step,
52
+ WasmEdge_ResultGetMessage(result));
53
+ exit(1);
54
+ }
55
+ }
56
+
57
+ // Called with the mutex held. Only release instances after execution ends.
58
+ static void reap(Threads *g) {
59
+ for (size_t i = 0; i < MAX_WORKERS; ++i) {
60
+ Worker *w = &g->workers[i];
61
+ if (!w->async || !WasmEdge_AsyncWaitFor(w->async, 0)) continue;
62
+ WasmEdge_Result result = WasmEdge_AsyncGet(w->async, NULL, 0);
63
+ if (WasmEdge_ResultGetCode(result) == WasmEdge_ErrCode_Terminated) {
64
+ g->exited = true;
65
+ g->closed = true;
66
+ } else if (!WasmEdge_ResultOK(result)) {
67
+ fprintf(stderr, "wasm trap: wasi worker: %s\n",
68
+ WasmEdge_ResultGetMessage(result));
69
+ g->failed = true;
70
+ g->closed = true;
71
+ }
72
+ WasmEdge_AsyncDelete(w->async);
73
+ WasmEdge_ModuleInstanceDelete(w->instance);
74
+ *w = (Worker){0};
75
+ }
76
+ }
77
+
78
+ static WasmEdge_Result thread_spawn(void *data,
79
+ const WasmEdge_CallingFrameContext *frame, const WasmEdge_Value *in,
80
+ WasmEdge_Value *out) {
81
+ Threads *g = data;
82
+ out[0] = WasmEdge_ValueGenI32(-6); // WASI EAGAIN: caller may retry or run inline.
83
+ pthread_mutex_lock(&g->mutex);
84
+ if (g->closed) goto done;
85
+ reap(g);
86
+ if (g->closed || g->next_tid >= (1 << 29)) goto done;
87
+ Worker *w = NULL;
88
+ for (size_t i = 0; i < MAX_WORKERS; ++i) {
89
+ if (!g->workers[i].async) { w = &g->workers[i]; break; }
90
+ }
91
+ if (!w) goto done;
92
+ WasmEdge_Result result = WasmEdge_ExecutorInstantiate(
93
+ WasmEdge_CallingFrameGetExecutor(frame), &w->instance, g->store, g->ast);
94
+ if (!WasmEdge_ResultOK(result)) goto discard;
95
+ const WasmEdge_FunctionInstanceContext *entry =
96
+ WasmEdge_ModuleInstanceFindFunction(w->instance, name("wasi_thread_start"));
97
+ if (!entry) goto discard;
98
+ int32_t tid = g->next_tid++;
99
+ WasmEdge_Value args[] = {WasmEdge_ValueGenI32(tid), in[0]};
100
+ w->async = WasmEdge_ExecutorAsyncInvoke(WasmEdge_CallingFrameGetExecutor(frame), entry, args, 2);
101
+ if (!w->async) goto discard;
102
+ out[0] = WasmEdge_ValueGenI32(tid);
103
+ goto done;
104
+ discard:
105
+ if (w->instance) WasmEdge_ModuleInstanceDelete(w->instance);
106
+ *w = (Worker){0};
107
+ done:
108
+ pthread_mutex_unlock(&g->mutex);
109
+ return WasmEdge_Result_Success;
110
+ }
111
+
112
+ static WasmEdge_ModuleInstanceContext *memory_import(Threads *g) {
113
+ uint32_t count = WasmEdge_ASTModuleListImportsLength(g->ast);
114
+ const WasmEdge_ImportTypeContext **imports = calloc(count, sizeof(*imports));
115
+ if (count && !imports) exit(1);
116
+ WasmEdge_ASTModuleListImports(g->ast, imports, count);
117
+ WasmEdge_ModuleInstanceContext *env = NULL;
118
+ bool threaded = false;
119
+ for (uint32_t i = 0; i < count; ++i) {
120
+ WasmEdge_String mod = WasmEdge_ImportTypeGetModuleName(imports[i]);
121
+ WasmEdge_String field = WasmEdge_ImportTypeGetExternalName(imports[i]);
122
+ if (WasmEdge_StringIsEqual(mod, name("wasi")) &&
123
+ WasmEdge_StringIsEqual(field, name("thread-spawn"))) threaded = true;
124
+ if (WasmEdge_ImportTypeGetExternalType(imports[i]) !=
125
+ WasmEdge_ExternalType_Memory) continue;
126
+ if (env || !WasmEdge_StringIsEqual(mod, name("env")) ||
127
+ !WasmEdge_StringIsEqual(field, name("memory"))) {
128
+ fprintf(stderr, "unsupported imported memory: expected one env.memory\n");
129
+ exit(1);
130
+ }
131
+ const WasmEdge_MemoryTypeContext *type =
132
+ WasmEdge_ImportTypeGetMemoryType(g->ast, imports[i]);
133
+ WasmEdge_Limit limit = WasmEdge_MemoryTypeGetLimit(type);
134
+ if (!limit.Shared || !limit.HasMax) {
135
+ fprintf(stderr, "WASI threads require bounded shared memory\n");
136
+ exit(1);
137
+ }
138
+ env = WasmEdge_ModuleInstanceCreate(name("env"));
139
+ WasmEdge_MemoryInstanceContext *memory = WasmEdge_MemoryInstanceCreate(type);
140
+ if (!env || !memory) exit(1);
141
+ WasmEdge_ModuleInstanceAddMemory(env, name("memory"), memory);
142
+ require_result("register memory", WasmEdge_ExecutorRegisterImport(
143
+ g->executor, g->store, env));
144
+ }
145
+ free(imports);
146
+ if (threaded && !env) {
147
+ fprintf(stderr, "WASI threads require imported shared memory\n");
148
+ exit(1);
149
+ }
150
+ return env;
151
+ }
152
+
153
+ // Direct calls use the same PIV request/response bytes as commands. Stage the
154
+ // request through the public allocator, then monitor the direct call with the
155
+ // same worker-group lifecycle as _start. No guest ABI is reimplemented. The
156
+ // module initializes first, once: a reactor through _initialize, a command
157
+ // artifact serving the direct surface through __wasm_call_ctors (its _start
158
+ // would run main).
159
+ static uint32_t stage_direct_request(Threads *g,
160
+ WasmEdge_ModuleInstanceContext *instance,
161
+ WasmEdge_MemoryInstanceContext *memory, WasmEdge_Value args[3]) {
162
+ size_t size = 0, capacity = 4096;
163
+ uint8_t *input = malloc(capacity);
164
+ if (!input) exit(1);
165
+ for (;;) {
166
+ size += fread(input + size, 1, capacity - size, stdin);
167
+ if (ferror(stdin)) { fprintf(stderr, "failed to read stdin\n"); exit(1); }
168
+ if (feof(stdin)) break;
169
+ if (capacity >= (1U << 30)) { fprintf(stderr, "request exceeds 1 GiB\n"); exit(1); }
170
+ capacity *= 2;
171
+ uint8_t *grown = realloc(input, capacity);
172
+ if (!grown) exit(1);
173
+ input = grown;
174
+ }
175
+ const WasmEdge_FunctionInstanceContext *init =
176
+ WasmEdge_ModuleInstanceFindFunction(instance, name("_initialize"));
177
+ if (!init) init = WasmEdge_ModuleInstanceFindFunction(instance, name("__wasm_call_ctors"));
178
+ if (init) require_result("initialize", WasmEdge_ExecutorInvoke(g->executor, init, NULL, 0, NULL, 0));
179
+ const WasmEdge_FunctionInstanceContext *alloc =
180
+ WasmEdge_ModuleInstanceFindFunction(instance, name("plugin_alloc"));
181
+ if (!alloc || !memory) { fprintf(stderr, "direct module requires plugin_alloc and memory\n"); exit(1); }
182
+ uint32_t padded = ((uint32_t)size + 7U) & ~7U;
183
+ WasmEdge_Value allocation_size = WasmEdge_ValueGenI32((int32_t)(padded + 4));
184
+ WasmEdge_Value pointer;
185
+ require_result("allocate", WasmEdge_ExecutorInvoke(g->executor, alloc, &allocation_size, 1, &pointer, 1));
186
+ uint32_t ptr = (uint32_t)WasmEdge_ValueGetI32(pointer);
187
+ if (!ptr || ptr % 8) { fprintf(stderr, "invalid direct allocation\n"); exit(1); }
188
+ uint32_t length_ptr = ptr + padded;
189
+ const uint8_t zero[4] = {0};
190
+ require_result("copy request", WasmEdge_MemoryInstanceSetData(memory, input, ptr, (uint32_t)size));
191
+ require_result("clear response length", WasmEdge_MemoryInstanceSetData(memory, zero, length_ptr, 4));
192
+ free(input);
193
+ args[0] = pointer;
194
+ args[1] = WasmEdge_ValueGenI32((int32_t)size);
195
+ args[2] = WasmEdge_ValueGenI32((int32_t)length_ptr);
196
+ return length_ptr;
197
+ }
198
+
199
+ static bool write_direct_response(WasmEdge_MemoryInstanceContext *memory,
200
+ uint32_t ptr, uint32_t length_ptr) {
201
+ uint8_t bytes[4];
202
+ require_result("read response length", WasmEdge_MemoryInstanceGetData(memory, bytes, length_ptr, 4));
203
+ uint32_t length = (uint32_t)bytes[0] | (uint32_t)bytes[1] << 8 |
204
+ (uint32_t)bytes[2] << 16 | (uint32_t)bytes[3] << 24;
205
+ if (!ptr || ptr % 8 || !length) {
206
+ fprintf(stderr, "invalid direct response\n");
207
+ return false;
208
+ }
209
+ const uint8_t *data = WasmEdge_MemoryInstanceGetPointerConst(memory, ptr, length);
210
+ if (!data) { fprintf(stderr, "direct response is out of bounds\n"); return false; }
211
+ return fwrite(data, 1, length, stdout) == length;
212
+ }
213
+
214
+ int main(int argc, char **argv) {
215
+ if (strcmp(WasmEdge_VersionGet(), "0.16.4") != 0) {
216
+ fprintf(stderr, "WASI threads runner requires WasmEdge 0.16.4; linked %s\n", WasmEdge_VersionGet());
217
+ return 2;
218
+ }
219
+ if (argc == 2 && strcmp(argv[1], "--version") == 0) {
220
+ printf("wasmedge version %s\nsdm-wasi-threads-runner 1\n",
221
+ WasmEdge_VersionGet());
222
+ return 0;
223
+ }
224
+ // WasmEdge's default logger writes to stdout. Guest stdout is binary; all
225
+ // host diagnostics below are emitted to stderr instead.
226
+ WasmEdge_LogOff();
227
+ const char **envs = calloc((size_t)argc, sizeof(*envs));
228
+ if (!envs) return 1;
229
+ uint32_t env_count = 0;
230
+ bool stats = false;
231
+ bool force_direct = false;
232
+ int first = 1;
233
+ for (; first < argc; ++first) {
234
+ if (strcmp(argv[first], "--enable-threads") == 0) continue;
235
+ if (strcmp(argv[first], "--sdm-thread-stats") == 0) { stats = true; continue; }
236
+ if (strcmp(argv[first], "--sdm-direct") == 0) { force_direct = true; continue; }
237
+ if (strcmp(argv[first], "--env") == 0 && first + 1 < argc) {
238
+ envs[env_count++] = argv[++first];
239
+ continue;
240
+ }
241
+ break;
242
+ }
243
+ if (first >= argc || argv[first][0] == '-') {
244
+ fprintf(stderr, "usage: %s [--sdm-direct] [--env NAME=VALUE] module.wasm [args...]\n", argv[0]);
245
+ free(envs);
246
+ return 2;
247
+ }
248
+ WasmEdge_ConfigureContext *conf = WasmEdge_ConfigureCreate();
249
+ WasmEdge_ConfigureAddProposal(conf, WasmEdge_Proposal_Threads);
250
+ WasmEdge_LoaderContext *loader = WasmEdge_LoaderCreate(conf);
251
+ WasmEdge_ValidatorContext *validator = WasmEdge_ValidatorCreate(conf);
252
+ Threads g = {.executor = WasmEdge_ExecutorCreate(conf, NULL),
253
+ .store = WasmEdge_StoreCreate(), .next_tid = 1};
254
+ if (pthread_mutex_init(&g.mutex, NULL) != 0) return 1;
255
+ require_result("parse", WasmEdge_LoaderParseFromFile(loader, &g.ast, argv[first]));
256
+ require_result("validate", WasmEdge_ValidatorValidate(validator, g.ast));
257
+ WasmEdge_ModuleInstanceContext *wasi = WasmEdge_ModuleInstanceCreateWASI(
258
+ (const char *const *)(argv + first), (uint32_t)(argc - first),
259
+ envs, env_count, NULL, 0);
260
+ free(envs);
261
+ if (!wasi) return 1;
262
+ require_result("register WASI", WasmEdge_ExecutorRegisterImport(
263
+ g.executor, g.store, wasi));
264
+ WasmEdge_ModuleInstanceContext *env = memory_import(&g);
265
+ WasmEdge_ModuleInstanceContext *threads = WasmEdge_ModuleInstanceCreate(name("wasi"));
266
+ WasmEdge_ValType i32 = WasmEdge_ValTypeGenI32();
267
+ WasmEdge_FunctionTypeContext *type = WasmEdge_FunctionTypeCreate(&i32, 1, &i32, 1);
268
+ WasmEdge_FunctionInstanceContext *spawn = WasmEdge_FunctionInstanceCreate(
269
+ type, thread_spawn, &g, 0);
270
+ WasmEdge_FunctionTypeDelete(type);
271
+ if (!threads || !spawn) return 1;
272
+ WasmEdge_ModuleInstanceAddFunction(threads, name("thread-spawn"), spawn);
273
+ require_result("register wasi threads", WasmEdge_ExecutorRegisterImport(
274
+ g.executor, g.store, threads));
275
+ WasmEdge_ModuleInstanceContext *instance = NULL;
276
+ require_result("instantiate", WasmEdge_ExecutorInstantiate(
277
+ g.executor, &instance, g.store, g.ast));
278
+ // --sdm-direct serves the direct surface of an artifact that also has a
279
+ // command entry; a reactor (no _start) is always served directly.
280
+ const WasmEdge_FunctionInstanceContext *start = force_direct ? NULL :
281
+ WasmEdge_ModuleInstanceFindFunction(instance, name("_start"));
282
+ bool direct = start == NULL;
283
+ WasmEdge_Value direct_args[3], response_pointer;
284
+ uint32_t response_length_ptr = 0;
285
+ WasmEdge_MemoryInstanceContext *memory = env
286
+ ? WasmEdge_ModuleInstanceFindMemory(env, name("memory"))
287
+ : WasmEdge_ModuleInstanceFindMemory(instance, name("memory"));
288
+ if (direct) {
289
+ start = WasmEdge_ModuleInstanceFindFunction(instance, name("plugin_invoke_stream"));
290
+ if (!start) { fprintf(stderr, "module has neither _start nor plugin_invoke_stream\n"); return 1; }
291
+ response_length_ptr = stage_direct_request(&g, instance, memory, direct_args);
292
+ }
293
+ WasmEdge_Async *main_call = WasmEdge_ExecutorAsyncInvoke(g.executor, start,
294
+ direct ? direct_args : NULL, direct ? 3 : 0);
295
+ if (!main_call) return 1;
296
+ for (;;) {
297
+ bool finished = WasmEdge_AsyncWaitFor(main_call, 1);
298
+ pthread_mutex_lock(&g.mutex);
299
+ reap(&g);
300
+ bool stop = finished || g.failed || g.exited;
301
+ if (stop) g.closed = true;
302
+ pthread_mutex_unlock(&g.mutex);
303
+ if (stop) break;
304
+ }
305
+ // Exit/trap in any thread ends the whole command. Cancel all executions
306
+ // before releasing shared memory; a stuck cancellation ends this process
307
+ // without freeing objects underneath live native workers.
308
+ uint64_t deadline = monotonic_ms() + 2000;
309
+ if (!WasmEdge_AsyncWaitFor(main_call, 0)) WasmEdge_AsyncCancel(main_call);
310
+ pthread_mutex_lock(&g.mutex);
311
+ for (size_t i = 0; i < MAX_WORKERS; ++i) {
312
+ if (g.workers[i].async) WasmEdge_AsyncCancel(g.workers[i].async);
313
+ }
314
+ pthread_mutex_unlock(&g.mutex);
315
+ if (!wait_until(main_call, deadline)) {
316
+ fprintf(stderr, "wasm trap: main cancellation timed out\n");
317
+ fflush(NULL);
318
+ _Exit(1);
319
+ }
320
+ WasmEdge_Result result = WasmEdge_AsyncGet(main_call,
321
+ direct ? &response_pointer : NULL, direct ? 1 : 0);
322
+ bool failed = g.failed || (!WasmEdge_ResultOK(result) && !g.exited);
323
+ if (failed) fprintf(stderr, "wasm trap: command: %s\n", WasmEdge_ResultGetMessage(result));
324
+ for (size_t i = 0; i < MAX_WORKERS; ++i) {
325
+ Worker *w = &g.workers[i];
326
+ if (!w->async) continue;
327
+ if (!wait_until(w->async, deadline)) {
328
+ fprintf(stderr, "wasm trap: worker cancellation timed out\n");
329
+ fflush(NULL);
330
+ _Exit(1);
331
+ }
332
+ WasmEdge_AsyncDelete(w->async);
333
+ WasmEdge_ModuleInstanceDelete(w->instance);
334
+ }
335
+ if (stats) fprintf(stderr, "sdm-wasi-threads: spawned=%d\n", g.next_tid - 1);
336
+ if (direct && !failed && !g.exited) {
337
+ failed = !write_direct_response(memory, (uint32_t)WasmEdge_ValueGetI32(response_pointer), response_length_ptr);
338
+ }
339
+ int exit_code = failed ? 1 : (int)WasmEdge_ModuleInstanceWASIGetExitCode(wasi);
340
+ WasmEdge_AsyncDelete(main_call);
341
+ WasmEdge_ModuleInstanceDelete(instance);
342
+ WasmEdge_ModuleInstanceDelete(threads);
343
+ if (env) WasmEdge_ModuleInstanceDelete(env);
344
+ WasmEdge_ModuleInstanceDelete(wasi);
345
+ WasmEdge_StoreDelete(g.store);
346
+ WasmEdge_ASTModuleDelete(g.ast);
347
+ WasmEdge_ExecutorDelete(g.executor);
348
+ WasmEdge_ValidatorDelete(validator);
349
+ WasmEdge_LoaderDelete(loader);
350
+ WasmEdge_ConfigureDelete(conf);
351
+ pthread_mutex_destroy(&g.mutex);
352
+ return exit_code;
353
+ }
@@ -91,6 +91,7 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
91
91
  let stdout = new Uint8Array(0);
92
92
  let stderr = new Uint8Array(0);
93
93
  let harness = null;
94
+ let spawnCount = null;
94
95
  try {
95
96
  // Plaintext hygiene: no per-case copy. createBrowserModuleHarness
96
97
  // compiles directly from a caller-supplied buffer without copying it
@@ -101,7 +102,10 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
101
102
  // would only add a fresh, unzeroed plaintext copy per run.
102
103
  harness = await createBrowserModuleHarness({
103
104
  wasmSource: moduleBytes,
104
- surface: "command",
105
+ surface: plan.surface ?? "command",
106
+ enableBrowserWasiThreads: true,
107
+ maxThreads: threadCount,
108
+ wasiThreadWorkerUrl: new URL("/wasi-thread-worker.js", globalThis.location.href).href,
105
109
  args: planCase.args,
106
110
  env: {
107
111
  ...planCase.env,
@@ -113,8 +117,11 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
113
117
  } catch (error) {
114
118
  ({ exitClass, exitDetail } = classifyBrowserError(error));
115
119
  } finally {
120
+ stderr = harness?.wasi?.stderr ?? stderr;
121
+ if (exitClass !== OK) stdout = harness?.wasi?.stdout ?? stdout;
122
+ spawnCount = harness?.threadHost?.spawnCount() ?? null;
116
123
  try {
117
- harness?.destroy?.();
124
+ await harness?.destroy?.();
118
125
  } catch {
119
126
  /* teardown must never mask the run result */
120
127
  }
@@ -124,6 +131,7 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
124
131
  threadCount,
125
132
  exitClass,
126
133
  exitDetail,
134
+ spawnCount,
127
135
  stdoutBase64: bytesToBase64(stdout),
128
136
  stderrBase64: bytesToBase64(stderr),
129
137
  });
@@ -144,8 +152,8 @@ async function postDone(body) {
144
152
  // Auto-run only inside a real browser page (the served runner). Node imports
145
153
  // of this module (tests, bundlers) never auto-run.
146
154
  if (
147
- typeof globalThis.document !== "undefined" &&
148
- typeof globalThis.window !== "undefined" &&
155
+ typeof globalThis.self !== "undefined" &&
156
+ typeof globalThis.location !== "undefined" &&
149
157
  globalThis.__SDM_PARITY_AUTORUN__ !== false
150
158
  ) {
151
159
  runParityPlanInBrowser({})
@@ -71,14 +71,18 @@ export function loadWasmEdgePin() {
71
71
  if (!repository) {
72
72
  throw new Error("wasmedgePin.json dockerImageRepository is required.");
73
73
  }
74
+ const runnerIdentity = createHash("sha256")
75
+ .update(readFileSync(path.join(__dirname, "native/wasmedge_wasi_threads_runner.c")))
76
+ .update(readFileSync(path.join(__dirname, "native/wasmedge-0.16.4-atomic-wait.patch")))
77
+ .update(readFileSync(path.join(__dirname, String(pin.dockerfile))))
78
+ .digest("hex").slice(0, 16);
74
79
  return Object.freeze({
75
80
  wasmedgeVersion: version,
76
81
  dockerImageRepository: repository,
77
82
  dockerImage: `${repository}:${version}`,
83
+ dockerRunnerImage: `${repository}:${version}-threads-${runnerIdentity}`,
78
84
  dockerfilePath: path.join(__dirname, String(pin.dockerfile)),
79
- dockerfileContextDir: path.dirname(
80
- path.join(__dirname, String(pin.dockerfile)),
81
- ),
85
+ dockerfileContextDir: __dirname,
82
86
  pinPath,
83
87
  });
84
88
  }
@@ -495,6 +499,11 @@ function applyInjectedDivergence(runs, injectDivergence) {
495
499
  * @param {string[]} [options.lanes] - subset of PARITY_LANES. Defaults to ALL
496
500
  * THREE. Narrowing is always an explicit caller decision — lanes are never
497
501
  * skipped silently, and a single-lane run can never claim parity.
502
+ * @param {string} [options.surface] - "direct" runs EVERY lane on the direct
503
+ * surface: the browser harness, and the wasi-threads runner with
504
+ * --sdm-direct (the WasmEdge CLI has no direct surface, so a single-thread
505
+ * artifact cannot be compared on it). Omitted: the browser lane follows
506
+ * browserSurface and the WasmEdge lanes run the artifact's default surface.
498
507
  * @param {Object} [options.laneRunners] - {laneName: async (context) => runs[]}
499
508
  * override for tests. Default runners come from parityLanes.js.
500
509
  * @param {string} [options.injectDivergence] - fire-drill: XOR one output byte
@@ -550,6 +559,10 @@ export async function runParityHarness(options = {}) {
550
559
  log: options.log ?? (() => {}),
551
560
  chromeBinary: options.chromeBinary,
552
561
  wasmedgeBinary: options.wasmedgeBinary,
562
+ wasmEdgeRunnerBinary: options.wasmEdgeRunnerBinary,
563
+ surface: options.surface ?? null,
564
+ browserSurface: options.browserSurface ?? options.surface ??
565
+ (WebAssembly.Module.exports(await WebAssembly.compile(loadableBytes)).some((entry) => entry.name === "_start") ? "command" : "direct"),
553
566
  dockerBinary: options.dockerBinary,
554
567
  dockerPlatform: options.dockerPlatform,
555
568
  autoBuildDockerImage: options.autoBuildDockerImage !== false,
@@ -609,6 +622,7 @@ export async function runParityHarness(options = {}) {
609
622
  threadCount: run.threadCount,
610
623
  exitClass: run.exitClass,
611
624
  exitDetail: run.exitDetail ?? null,
625
+ spawnCount: run.spawnCount ?? null,
612
626
  stdoutSha256: sha256Hex(toUint8Array(run.stdout) ?? new Uint8Array(0)),
613
627
  stdoutLength: (toUint8Array(run.stdout) ?? new Uint8Array(0)).length,
614
628
  })),
@@ -30,6 +30,8 @@ import { promisify } from "node:util";
30
30
 
31
31
  import { ExitClass, assertWasmEdgeVersionMatchesPin } from "./parityHarness.js";
32
32
  import { normalizeWasmEdgeOutcome } from "./wasmedgeOutput.js";
33
+ import { isWasiThreadsModule } from "../host/wasiThreadHost.js";
34
+ import { resolveWasmEdgeWasiThreadsRunner } from "./buildWasmEdgeRunner.js";
33
35
 
34
36
  const execFile = promisify(execFileCallback);
35
37
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -128,6 +130,21 @@ function wasmedgeInvocationArgs(planCase, plan, threadCount) {
128
130
  return args;
129
131
  }
130
132
 
133
+ // Runner flags that select the surface. Only the wasi-threads runner serves the
134
+ // direct surface of an artifact with a command entry; the WasmEdge CLI runs
135
+ // `_start` or nothing, so a direct-surface comparison on it would silently
136
+ // compare two different surfaces.
137
+ function surfaceArgs(context, threaded, lane) {
138
+ if (context.surface !== "direct") return [];
139
+ if (!threaded) {
140
+ throw new Error(
141
+ `${lane}: the WasmEdge CLI serves only the command surface of a single-thread artifact; ` +
142
+ "it cannot take part in a direct-surface parity run.",
143
+ );
144
+ }
145
+ return ["--sdm-direct"];
146
+ }
147
+
131
148
  async function stageModuleWorkdir(context, label) {
132
149
  const workdir = await mkdtemp(path.join(os.tmpdir(), `sdm-parity-${label}-`));
133
150
  // Stage the canonical loadable bytes (publication records stripped by the
@@ -162,7 +179,10 @@ export async function resolveWasmEdgeBinary(context = {}) {
162
179
  }
163
180
 
164
181
  export async function runNativeWasmEdgeLane(context) {
165
- const binary = await resolveWasmEdgeBinary(context);
182
+ const threaded = isWasiThreadsModule(await WebAssembly.compile(context.loadableBytes ?? context.wasmBytes));
183
+ const binary = threaded
184
+ ? await resolveWasmEdgeWasiThreadsRunner(context)
185
+ : await resolveWasmEdgeBinary(context);
166
186
  let versionOutput;
167
187
  try {
168
188
  versionOutput = (await execFile(binary, ["--version"])).stdout;
@@ -178,6 +198,7 @@ export async function runNativeWasmEdgeLane(context) {
178
198
  `native binary ${binary}`,
179
199
  );
180
200
 
201
+ const directArgs = surfaceArgs(context, threaded, "native WasmEdge lane");
181
202
  const workdir = await stageModuleWorkdir(context, "native");
182
203
  const runs = [];
183
204
  try {
@@ -185,7 +206,9 @@ export async function runNativeWasmEdgeLane(context) {
185
206
  for (const threadCount of planCase.threadCounts) {
186
207
  const outcome = await spawnWithStdin(
187
208
  binary,
188
- wasmedgeInvocationArgs(planCase, context.plan, threadCount),
209
+ [...(threaded ? ["--sdm-thread-stats"] : []),
210
+ ...directArgs,
211
+ ...wasmedgeInvocationArgs(planCase, context.plan, threadCount)],
189
212
  {
190
213
  cwd: workdir,
191
214
  env: { PATH: process.env.PATH ?? "" },
@@ -208,6 +231,7 @@ export async function runNativeWasmEdgeLane(context) {
208
231
  exitDetail,
209
232
  stdout: normalized.stdout,
210
233
  stderr: normalized.stderr,
234
+ spawnCount: threaded ? Number(/sdm-wasi-threads: spawned=(\d+)/.exec(normalized.diagnosticText)?.[1] ?? NaN) : null,
211
235
  stateFiles: null,
212
236
  });
213
237
  }
@@ -271,6 +295,8 @@ export async function ensureDockerParityImage(context) {
271
295
  }
272
296
 
273
297
  export async function runDockerWasmEdgeLane(context) {
298
+ const threaded = isWasiThreadsModule(await WebAssembly.compile(context.loadableBytes ?? context.wasmBytes));
299
+ if (threaded) context = { ...context, pin: { ...context.pin, dockerImage: context.pin.dockerRunnerImage } };
274
300
  const dockerBinary = context.dockerBinary ?? "docker";
275
301
  try {
276
302
  await execFile(dockerBinary, ["--version"]);
@@ -279,6 +305,7 @@ export async function runDockerWasmEdgeLane(context) {
279
305
  `docker WasmEdge lane: cannot execute "${dockerBinary}" (${error.message}).`,
280
306
  );
281
307
  }
308
+ const directArgs = surfaceArgs(context, threaded, "docker WasmEdge lane");
282
309
  const image = await ensureDockerParityImage(context);
283
310
 
284
311
  const workdir = await stageModuleWorkdir(context, "docker");
@@ -300,8 +327,11 @@ export async function runDockerWasmEdgeLane(context) {
300
327
  if (context.dockerPlatform) {
301
328
  dockerArgs.push("--platform", String(context.dockerPlatform));
302
329
  }
330
+ if (threaded) dockerArgs.push("--entrypoint", "/opt/wasmedge/bin/sdm-wasi-threads-runner");
303
331
  dockerArgs.push(
304
332
  image,
333
+ ...(threaded ? ["--sdm-thread-stats"] : []),
334
+ ...directArgs,
305
335
  ...wasmedgeInvocationArgs(planCase, context.plan, threadCount),
306
336
  );
307
337
  const outcome = await spawnWithStdin(dockerBinary, dockerArgs, {
@@ -323,6 +353,7 @@ export async function runDockerWasmEdgeLane(context) {
323
353
  exitDetail,
324
354
  stdout: normalized.stdout,
325
355
  stderr: normalized.stderr,
356
+ spawnCount: threaded ? Number(/sdm-wasi-threads: spawned=(\d+)/.exec(normalized.diagnosticText)?.[1] ?? NaN) : null,
326
357
  stateFiles: null,
327
358
  });
328
359
  }
@@ -373,11 +404,17 @@ async function buildBrowserRunnerBundle() {
373
404
  external: ["node:*", "hd-wallet-wasm"],
374
405
  logLevel: "silent",
375
406
  });
376
- return result.outputFiles[0].text;
407
+ const threadWorker = await esbuild.build({
408
+ entryPoints: [path.join(__dirname, "../host/wasiThreadBrowserWorker.mjs")],
409
+ bundle: true, write: false, format: "esm", platform: "browser", target: ["chrome110"],
410
+ external: ["node:*", "hd-wallet-wasm"], logLevel: "silent",
411
+ });
412
+ return { runner: result.outputFiles[0].text, threadWorker: threadWorker.outputFiles[0].text };
377
413
  }
378
414
 
379
415
  function browserPlanPayload(context) {
380
416
  return JSON.stringify({
417
+ surface: context.browserSurface ?? "command",
381
418
  threadEnvVar: context.plan.threadEnvVar,
382
419
  cases: context.plan.cases.map((planCase) => ({
383
420
  id: planCase.id,
@@ -393,7 +430,7 @@ const RUNNER_HTML = `<!doctype html>
393
430
  <html>
394
431
  <head><meta charset="utf-8"><title>sdm parity runner</title></head>
395
432
  <body><pre id="status">parity runner booting…</pre>
396
- <script type="module" src="/runner.js"></script></body>
433
+ <script type="module">new Worker("/runner.js", { type: "module" });</script></body>
397
434
  </html>`;
398
435
 
399
436
  export async function runBrowserLane(context) {
@@ -447,7 +484,12 @@ export async function runBrowserLane(context) {
447
484
  ...securityHeaders,
448
485
  "Content-Type": "text/javascript; charset=utf-8",
449
486
  });
450
- response.end(runnerBundle);
487
+ response.end(runnerBundle.runner);
488
+ return;
489
+ }
490
+ if (url.pathname === "/wasi-thread-worker.js") {
491
+ response.writeHead(200, { ...securityHeaders, "Content-Type": "text/javascript; charset=utf-8" });
492
+ response.end(runnerBundle.threadWorker);
451
493
  return;
452
494
  }
453
495
  if (url.pathname === "/plan") {
@@ -522,6 +564,7 @@ export async function runBrowserLane(context) {
522
564
  threadCount: Number(run.threadCount),
523
565
  exitClass: String(run.exitClass),
524
566
  exitDetail: run.exitDetail ?? null,
567
+ spawnCount: run.spawnCount ?? null,
525
568
  stdout: new Uint8Array(Buffer.from(String(run.stdoutBase64 ?? ""), "base64")),
526
569
  stderr: new Uint8Array(Buffer.from(String(run.stderrBase64 ?? ""), "base64")),
527
570
  stateFiles: null,