wasm-idle 0.4.0 → 0.5.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 (49) hide show
  1. package/README.md +111 -2
  2. package/dist/clang/app.d.ts +4 -0
  3. package/dist/clang/app.js +330 -15
  4. package/dist/clang/index.d.ts +10 -0
  5. package/dist/clang/index.js +284 -25
  6. package/dist/debug/controller.svelte.d.ts +43 -0
  7. package/dist/debug/controller.svelte.js +268 -0
  8. package/dist/debug/editor/index.d.ts +2 -1
  9. package/dist/debug/editor/index.js +1 -1
  10. package/dist/debug/editor/monaco.d.ts +8 -0
  11. package/dist/debug/editor/monaco.js +23 -0
  12. package/dist/debug/expression.d.ts +24 -0
  13. package/dist/debug/expression.js +307 -138
  14. package/dist/debug/index.d.ts +4 -1
  15. package/dist/debug/index.js +2 -1
  16. package/dist/index.d.ts +6 -3
  17. package/dist/index.js +3 -3
  18. package/dist/playground/assetBridge.d.ts +20 -0
  19. package/dist/playground/assetBridge.js +192 -0
  20. package/dist/playground/assets.d.ts +37 -0
  21. package/dist/playground/assets.js +105 -0
  22. package/dist/playground/clang.d.ts +11 -1
  23. package/dist/playground/clang.js +58 -4
  24. package/dist/playground/index.d.ts +5 -5
  25. package/dist/playground/index.js +48 -6
  26. package/dist/playground/java.d.ts +7 -2
  27. package/dist/playground/java.js +31 -5
  28. package/dist/playground/javaStdin.js +235 -2
  29. package/dist/playground/options.d.ts +18 -0
  30. package/dist/playground/options.js +15 -1
  31. package/dist/playground/python.d.ts +7 -1
  32. package/dist/playground/python.js +50 -5
  33. package/dist/playground/rust.d.ts +32 -0
  34. package/dist/playground/rust.js +175 -0
  35. package/dist/playground/sandbox.d.ts +19 -4
  36. package/dist/playground/wasmRustVersion.d.ts +1 -0
  37. package/dist/playground/wasmRustVersion.js +1 -0
  38. package/dist/playground/worker/assets.d.ts +12 -0
  39. package/dist/playground/worker/assets.js +257 -0
  40. package/dist/playground/worker/clang.js +22 -6
  41. package/dist/playground/worker/java.js +15 -35
  42. package/dist/playground/worker/python.js +13 -33
  43. package/dist/playground/worker/rust.d.ts +1 -0
  44. package/dist/playground/worker/rust.js +159 -0
  45. package/dist/terminal/Terminal.svelte +121 -18
  46. package/dist/terminal/Terminal.svelte.d.ts +4 -0
  47. package/dist/terminal/theme.js +2 -2
  48. package/dist/terminal/types.d.ts +4 -0
  49. package/package.json +19 -13
package/README.md CHANGED
@@ -2,10 +2,119 @@
2
2
 
3
3
  ![wasm-idle](static/image.jpeg)
4
4
 
5
- Executes C++, Python, and Java code with working stdio.
5
+ Executes C++, Python, Java, and Rust code with working stdio.
6
6
 
7
7
  Refer to src/lib/clang.
8
8
 
9
9
  Java uses TeaVM's browser compiler/runtime. TeaVM compiler/runtime/classlib assets are bundled under `static/teavm/` by default, and the asset base URL can be overridden with `PUBLIC_TEAVM_BASE_URL`.
10
10
 
11
- Powered by [wasm-clang](https://github.com/binji/wasm-clang), Pyodide, and TeaVM.
11
+ ## Rust browser integration
12
+
13
+ The demo app now bundles a local `wasm-rust` browser compiler under `static/wasm-rust/` and points the example `Terminal` at `/wasm-rust/index.js` by default. Refresh that bundle after rebuilding the sibling `wasm-rust` project with:
14
+
15
+ ```bash
16
+ cd wasm-idle
17
+ pnpm run sync:wasm-rust
18
+ ```
19
+
20
+ The built-in Rust route now supports both `wasm32-wasip1` and `wasm32-wasip2`. The page exposes a
21
+ target selector when Rust is active, defaults to `wasm32-wasip1`, and persists that choice in local
22
+ storage.
23
+
24
+ ## Browser regression commands
25
+
26
+ Browser-level Rust checks are reproducible from this repo:
27
+
28
+ ```bash
29
+ cd wasm-idle
30
+ pnpm run probe:rust-browser
31
+ pnpm run test:browser:playwright
32
+
33
+ WASM_IDLE_BROWSER_URL='http://localhost:5173/absproxy/5173/' \
34
+ WASM_IDLE_REUSE_LOCAL_PREVIEW=1 \
35
+ pnpm run probe:rust-browser
36
+
37
+ WASM_IDLE_RUN_REAL_BROWSER_RUST=1 \
38
+ WASM_IDLE_BROWSER_URL='http://localhost:5173/absproxy/5173/' \
39
+ WASM_IDLE_REUSE_LOCAL_PREVIEW=1 \
40
+ pnpm exec vitest run src/lib/playground/rust.playwright.test.ts
41
+ ```
42
+
43
+ Both commands exercise the real Chromium page path. The default Rust probe now feeds stdin with a
44
+ single line (`5\n`) and expects the page to finish without sending EOF, which keeps the regression
45
+ aligned with the default Rust sample and proves that pressing Enter is enough for line-based stdin.
46
+ Programs that intentionally read stdin until EOF can still be finished with `Ctrl+D` or the toolbar
47
+ `Send EOF` button while the process is running.
48
+ The browser helper writes stdin through the page-owned `window.__wasmIdleDebug.writeTerminalInput(...)`
49
+ hook instead of trying to click xterm's hidden helper textarea, which proved too flaky for repeatable
50
+ Playwright runs.
51
+ If Rust ever reports `invalid metadata files for crate core` or `Unsupported archive identifier`,
52
+ the browser almost always fetched a stale or wrong `wasm-rust` sysroot asset. Hard refresh the page
53
+ and resync `static/wasm-rust/` from the sibling `wasm-rust/dist/`.
54
+ When browser-rustc does retry, it now emits a visible warning instead of only a debug-level
55
+ transition into attempt `2/5`, `3/5`, and so on.
56
+ When the Rust `log` option is enabled, those compile-time `wasm-rust` progress and retry lines are
57
+ also forwarded into the terminal transcript before the final runtime output, so the browser console
58
+ is no longer required to inspect build progress.
59
+ They also resync the vendored `wasm-rust` bundle and rebuild `wasm-idle` first, so the browser run is
60
+ checked against the current assets rather than a stale preview output.
61
+ The probe/test helper also claims a dedicated local preview port by default instead of reusing
62
+ whatever already answers on `localhost`, which keeps the regression target tied to the current build.
63
+ If you point the probe at `dev.seorii.io`, remember that route currently requires an authenticated
64
+ session; the repo-owned regression target is the local preview path above.
65
+
66
+ ## Runtime expectations
67
+
68
+ Rust still supports an external browser compiler module for library consumers. Point `PUBLIC_WASM_RUST_COMPILER_URL` at a built `wasm-rust` ESM entry such as `.../wasm-rust/dist/index.js`, or pass `runtimeAssets.rust.compilerUrl` at runtime.
69
+
70
+ The Rust browser path now executes returned artifacts through the target-appropriate runtime inside
71
+ the Rust worker:
72
+
73
+ - `wasm32-wasip1` runs as preview1 core wasm through `@bjorn3/browser_wasi_shim`
74
+ - `wasm32-wasip2` runs as a preview2 component through `preview2-shim` plus transpiled `jco` output
75
+
76
+ The generic `src/lib/clang/app.ts` host remains in place for other runtimes, but Rust now delegates
77
+ execution to `wasm-rust` so the selected target and returned artifact format stay aligned.
78
+
79
+ `Terminal` and `playground(...).load(...)` support either the legacy shared `path`/`rootUrl` or per-runtime asset config:
80
+
81
+ ```ts
82
+ import type { PlaygroundRuntimeAssets } from 'wasm-idle';
83
+
84
+ const runtimeAssets: PlaygroundRuntimeAssets = {
85
+ rootUrl: 'https://cdn.example.com/repl',
86
+ python: {
87
+ loader: async ({ asset }) => ({ url: `https://cdn.example.com/repl/pyodide/${asset}` })
88
+ },
89
+ java: {
90
+ baseUrl: 'https://cdn.example.com/repl/teavm/'
91
+ },
92
+ rust: {
93
+ compilerUrl: 'https://cdn.example.com/wasm-rust/index.js'
94
+ }
95
+ };
96
+ ```
97
+
98
+ Python custom loaders receive file names under the Pyodide asset root and can serve both core assets and package files. TeaVM custom loaders receive file names under the TeaVM asset root. Rust expects a browser-loadable compiler module URL; that module is responsible for serving its own nested runtime assets. Compressed TeaVM runtime assets are no longer unpacked inside the library; provide the final file URL or handle decompression in your own loader.
99
+
100
+ If you want a host app to reuse the same runtime asset configuration for both `<Terminal>` and direct `playground(...)` access, bind it once:
101
+
102
+ ```ts
103
+ import Terminal, { createPlaygroundBinding } from 'wasm-idle';
104
+
105
+ const wasmIdle = createPlaygroundBinding({
106
+ rootUrl: 'https://cdn.example.com/repl',
107
+ rust: {
108
+ compilerUrl: 'https://cdn.example.com/wasm-rust/index.js'
109
+ }
110
+ });
111
+
112
+ const sandbox = await wasmIdle.load('PYTHON');
113
+ await sandbox.load('print("hi")', false);
114
+ ```
115
+
116
+ ```svelte
117
+ <Terminal {...wasmIdle.terminalProps} bind:terminal />
118
+ ```
119
+
120
+ Powered by [wasm-clang](https://github.com/binji/wasm-clang), Pyodide, TeaVM, and `wasm-rust`.
@@ -3,7 +3,10 @@ import { Memory, type MemFS } from './memory';
3
3
  interface DebugSession {
4
4
  buffer?: Int32Array;
5
5
  interruptBuffer?: Uint8Array;
6
+ watchBuffer?: Int32Array;
7
+ watchResultBuffer?: Int32Array;
6
8
  breakpoints: Set<number>;
9
+ breakpointVersion: number;
7
10
  pauseOnEntry: boolean;
8
11
  stepArmed: boolean;
9
12
  nextLineArmed: boolean;
@@ -52,6 +55,7 @@ export default class App {
52
55
  constructor(module: WebAssembly.Module, memfs: MemFS, name: string, ...args: string[]);
53
56
  run(): Promise<boolean | undefined>;
54
57
  proc_exit(code: number): void;
58
+ debugEvaluate(expression: string): string;
55
59
  private pauseDebugSession;
56
60
  __wasm_idle_debug_enter(functionId: number, line: number): number;
57
61
  __wasm_idle_debug_leave(functionId: number): number;
package/dist/clang/app.js CHANGED
@@ -2,6 +2,8 @@ import { bindNew } from './apply';
2
2
  import { Memory } from './memory';
3
3
  import { getInstance } from './wasm';
4
4
  import { AbortError, NotImplemented, ProcExit } from './error';
5
+ import { evaluateDebugExpressionWithResolver, parseStoredDebugValue } from '../debug/expression';
6
+ import { flushQueuedStdin, readBufferedStdin } from '../playground/stdinBuffer';
5
7
  const ESUCCESS = 0;
6
8
  const RAF_PROC_EXIT_CODE = 0xc0c0a;
7
9
  export default class App {
@@ -21,11 +23,17 @@ export default class App {
21
23
  this.environ = { USER: 'jungol' };
22
24
  this.memfs = memfs;
23
25
  const env = bindNew(this, '__wasm_idle_debug_enter', '__wasm_idle_debug_leave', '__wasm_idle_debug_line', '__wasm_idle_debug_value_num', '__wasm_idle_debug_value_bool', '__wasm_idle_debug_value_addr', '__wasm_idle_debug_value_text');
24
- const wasi_unstable = {
26
+ const wasi = {
25
27
  ...bindNew(this, 'proc_exit', 'environ_sizes_get', 'environ_get', 'args_sizes_get', 'args_get', 'random_get', 'clock_time_get', 'poll_oneoff'),
26
28
  ...this.memfs.exports
27
29
  };
28
- this.ready = getInstance(module, { wasi_unstable, env }).then((instance) => {
30
+ // Rust/WASI modules import `wasi_snapshot_preview1`, while older toolchains here still use
31
+ // `wasi_unstable`. Expose the same host under both names so either artifact shape runs.
32
+ this.ready = getInstance(module, {
33
+ wasi_unstable: wasi,
34
+ wasi_snapshot_preview1: wasi,
35
+ env
36
+ }).then((instance) => {
29
37
  this.instance = instance;
30
38
  this.exports = this.instance.exports;
31
39
  this.mem = new Memory(this.exports.memory);
@@ -66,15 +74,125 @@ export default class App {
66
74
  this.trace('start() returned without proc_exit');
67
75
  }
68
76
  proc_exit(code) {
77
+ this.trace(`proc_exit_throw(code=${code})`);
69
78
  throw new ProcExit(code);
70
79
  }
80
+ debugEvaluate(expression) {
81
+ const session = this.debugSession;
82
+ if (!session)
83
+ throw new Error('unavailable');
84
+ const frame = [...session.frames]
85
+ .reverse()
86
+ .find((candidate) => candidate.functionId === session.currentFunctionId);
87
+ const activeLine = session.currentLine;
88
+ const activeLocals = [...(session.variableMetadata[session.currentFunctionId] || [])]
89
+ .reverse()
90
+ .filter((variable) => activeLine >= variable.fromLine && activeLine <= variable.toLine);
91
+ const activeGlobals = [...(session.globalVariableMetadata || [])]
92
+ .reverse()
93
+ .filter((variable) => activeLine >= variable.fromLine && activeLine <= variable.toLine);
94
+ return evaluateDebugExpressionWithResolver(expression, (name) => {
95
+ const resolveArrayValue = (variable, addressValue) => {
96
+ const dimensions = variable.dimensions?.length
97
+ ? variable.dimensions
98
+ : variable.length
99
+ ? [variable.length]
100
+ : [];
101
+ const address = Number(addressValue);
102
+ if (!Number.isFinite(address) ||
103
+ address <= 0 ||
104
+ !dimensions.length ||
105
+ (!variable.elementKind && !variable.structFields?.length)) {
106
+ throw new Error('unavailable');
107
+ }
108
+ this.mem?.check?.();
109
+ const scalarSize = variable.structFields?.length && variable.structSize
110
+ ? variable.structSize
111
+ : variable.elementKind === 'double'
112
+ ? 8
113
+ : variable.elementKind === 'bool' || variable.elementKind === 'char'
114
+ ? 1
115
+ : 4;
116
+ const readScalar = (kind, offset) => {
117
+ if (kind === 'bool')
118
+ return !!this.mem.read8(offset);
119
+ if (kind === 'char') {
120
+ const charCode = this.mem.read8(offset);
121
+ return charCode >= 0x20 && charCode <= 0x7e
122
+ ? String.fromCharCode(charCode)
123
+ : charCode;
124
+ }
125
+ if (kind === 'float')
126
+ return this.mem.readFloat32(offset);
127
+ if (kind === 'double')
128
+ return this.mem.readFloat64(offset);
129
+ return this.mem.readInt32(offset);
130
+ };
131
+ const buildStructValue = (baseAddress) => ({
132
+ __debugExpressionKind: 'object',
133
+ has: (fieldName) => !!variable.structFields?.some((field) => field.name === fieldName),
134
+ get: (fieldName) => {
135
+ const field = variable.structFields?.find((candidate) => candidate.name === fieldName);
136
+ if (!field)
137
+ throw new Error('unavailable');
138
+ return readScalar(field.kind, baseAddress + field.offset);
139
+ },
140
+ keys: () => variable.structFields?.map((field) => field.name) || []
141
+ });
142
+ const buildArrayValue = (baseAddress, remainingDimensions) => ({
143
+ __debugExpressionKind: 'array',
144
+ length: remainingDimensions[0],
145
+ truncated: remainingDimensions[0] > 8,
146
+ get: (index) => {
147
+ if (!Number.isInteger(index) ||
148
+ index < 0 ||
149
+ index >= remainingDimensions[0]) {
150
+ throw new Error('unavailable');
151
+ }
152
+ if (remainingDimensions.length > 1) {
153
+ const nestedStride = remainingDimensions
154
+ .slice(1)
155
+ .reduce((total, size) => total * size, 1) * scalarSize;
156
+ return buildArrayValue(baseAddress + index * nestedStride, remainingDimensions.slice(1));
157
+ }
158
+ if (variable.structFields?.length && variable.structSize) {
159
+ return buildStructValue(baseAddress + index * variable.structSize);
160
+ }
161
+ if (!variable.elementKind)
162
+ throw new Error('unavailable');
163
+ return readScalar(variable.elementKind, baseAddress + index * scalarSize);
164
+ },
165
+ keys: () => Array.from({ length: Math.min(remainingDimensions[0], 8) }, (_, index) => index)
166
+ });
167
+ return buildArrayValue(address, dimensions);
168
+ };
169
+ const resolveVariableValue = (variable, storedValue) => {
170
+ if (storedValue == null || storedValue === '?')
171
+ throw new Error('unavailable');
172
+ if (variable.kind === 'array')
173
+ return resolveArrayValue(variable, storedValue);
174
+ return parseStoredDebugValue(storedValue);
175
+ };
176
+ const localVariable = activeLocals.find((variable) => variable.name === name);
177
+ if (localVariable) {
178
+ return resolveVariableValue(localVariable, frame?.values.get(localVariable.slot));
179
+ }
180
+ const globalVariable = activeGlobals.find((variable) => variable.name === name);
181
+ if (globalVariable) {
182
+ return resolveVariableValue(globalVariable, session.globalValues.get(globalVariable.slot));
183
+ }
184
+ throw new Error('unavailable');
185
+ });
186
+ }
71
187
  pauseDebugSession(session, functionId, line, reason) {
72
188
  const buffer = session.buffer;
73
189
  if (!buffer)
74
190
  return ESUCCESS;
75
191
  session.currentFunctionId = functionId;
76
192
  session.currentLine = line;
77
- const frame = [...session.frames].reverse().find((candidate) => candidate.functionId === functionId);
193
+ const frame = [...session.frames]
194
+ .reverse()
195
+ .find((candidate) => candidate.functionId === functionId);
78
196
  if (frame)
79
197
  frame.line = line;
80
198
  session.pauseOnEntry = false;
@@ -93,9 +211,49 @@ export default class App {
93
211
  : variable.length
94
212
  ? [variable.length]
95
213
  : [];
96
- if (!Number.isFinite(address) || address <= 0 || !dimensions.length || !variable.elementKind) {
214
+ if (!Number.isFinite(address) ||
215
+ address <= 0 ||
216
+ !dimensions.length ||
217
+ (!variable.elementKind && !variable.structFields?.length)) {
97
218
  return [{ name: variable.name, value: '?' }];
98
219
  }
220
+ if (variable.structFields?.length && variable.structSize) {
221
+ const previewLength = Math.min(dimensions[0], 8);
222
+ const values = [];
223
+ for (let index = 0; index < previewLength; index += 1) {
224
+ const fieldValues = [];
225
+ for (const field of variable.structFields) {
226
+ const offset = address + index * variable.structSize + field.offset;
227
+ if (field.kind === 'bool') {
228
+ fieldValues.push(`${field.name}: ${this.mem.read8(offset) ? 'true' : 'false'}`);
229
+ continue;
230
+ }
231
+ if (field.kind === 'char') {
232
+ const charCode = this.mem.read8(offset);
233
+ fieldValues.push(`${field.name}: ${charCode >= 0x20 && charCode <= 0x7e ? `'${String.fromCharCode(charCode)}'` : `${charCode}`}`);
234
+ continue;
235
+ }
236
+ if (field.kind === 'float') {
237
+ fieldValues.push(`${field.name}: ${this.mem.readFloat32(offset)}`);
238
+ continue;
239
+ }
240
+ if (field.kind === 'double') {
241
+ fieldValues.push(`${field.name}: ${this.mem.readFloat64(offset)}`);
242
+ continue;
243
+ }
244
+ fieldValues.push(`${field.name}: ${this.mem.readInt32(offset)}`);
245
+ }
246
+ values.push(`{${fieldValues.join(', ')}}`);
247
+ }
248
+ return [
249
+ {
250
+ name: variable.name,
251
+ value: `[${values.join(', ')}${dimensions[0] > previewLength ? ', ...' : ''}]`
252
+ }
253
+ ];
254
+ }
255
+ if (!variable.elementKind)
256
+ return [{ name: variable.name, value: '?' }];
99
257
  const elementStride = variable.elementKind === 'double'
100
258
  ? 8
101
259
  : variable.elementKind === 'bool' || variable.elementKind === 'char'
@@ -115,7 +273,9 @@ export default class App {
115
273
  }
116
274
  if (variable.elementKind === 'char') {
117
275
  const charCode = this.mem.read8(offset);
118
- values.push(charCode >= 0x20 && charCode <= 0x7e ? `'${String.fromCharCode(charCode)}'` : `${charCode}`);
276
+ values.push(charCode >= 0x20 && charCode <= 0x7e
277
+ ? `'${String.fromCharCode(charCode)}'`
278
+ : `${charCode}`);
119
279
  continue;
120
280
  }
121
281
  if (variable.elementKind === 'float') {
@@ -147,7 +307,9 @@ export default class App {
147
307
  }
148
308
  if (variable.elementKind === 'char') {
149
309
  const charCode = this.mem.read8(offset);
150
- values.push(charCode >= 0x20 && charCode <= 0x7e ? `'${String.fromCharCode(charCode)}'` : `${charCode}`);
310
+ values.push(charCode >= 0x20 && charCode <= 0x7e
311
+ ? `'${String.fromCharCode(charCode)}'`
312
+ : `${charCode}`);
151
313
  continue;
152
314
  }
153
315
  if (variable.elementKind === 'float') {
@@ -176,6 +338,132 @@ export default class App {
176
338
  return [];
177
339
  if (line < variable.fromLine || line > variable.toLine)
178
340
  return [];
341
+ if (variable.kind === 'array') {
342
+ this.mem?.check?.();
343
+ const address = Number(session.globalValues?.get(variable.slot) ?? Number.NaN);
344
+ const dimensions = variable.dimensions?.length
345
+ ? variable.dimensions
346
+ : variable.length
347
+ ? [variable.length]
348
+ : [];
349
+ if (!Number.isFinite(address) ||
350
+ address <= 0 ||
351
+ !dimensions.length ||
352
+ (!variable.elementKind && !variable.structFields?.length)) {
353
+ return [{ name: variable.name, value: '?' }];
354
+ }
355
+ if (variable.structFields?.length && variable.structSize) {
356
+ const previewLength = Math.min(dimensions[0], 8);
357
+ const values = [];
358
+ for (let index = 0; index < previewLength; index += 1) {
359
+ const fieldValues = [];
360
+ for (const field of variable.structFields) {
361
+ const offset = address + index * variable.structSize + field.offset;
362
+ if (field.kind === 'bool') {
363
+ fieldValues.push(`${field.name}: ${this.mem.read8(offset) ? 'true' : 'false'}`);
364
+ continue;
365
+ }
366
+ if (field.kind === 'char') {
367
+ const charCode = this.mem.read8(offset);
368
+ fieldValues.push(`${field.name}: ${charCode >= 0x20 && charCode <= 0x7e ? `'${String.fromCharCode(charCode)}'` : `${charCode}`}`);
369
+ continue;
370
+ }
371
+ if (field.kind === 'float') {
372
+ fieldValues.push(`${field.name}: ${this.mem.readFloat32(offset)}`);
373
+ continue;
374
+ }
375
+ if (field.kind === 'double') {
376
+ fieldValues.push(`${field.name}: ${this.mem.readFloat64(offset)}`);
377
+ continue;
378
+ }
379
+ fieldValues.push(`${field.name}: ${this.mem.readInt32(offset)}`);
380
+ }
381
+ values.push(`{${fieldValues.join(', ')}}`);
382
+ }
383
+ return [
384
+ {
385
+ name: variable.name,
386
+ value: `[${values.join(', ')}${dimensions[0] > previewLength ? ', ...' : ''}]`
387
+ }
388
+ ];
389
+ }
390
+ if (!variable.elementKind)
391
+ return [{ name: variable.name, value: '?' }];
392
+ const elementStride = variable.elementKind === 'double'
393
+ ? 8
394
+ : variable.elementKind === 'bool' || variable.elementKind === 'char'
395
+ ? 1
396
+ : 4;
397
+ if (dimensions.length === 2) {
398
+ const previewRows = Math.min(dimensions[0], 4);
399
+ const previewCols = Math.min(dimensions[1], 8);
400
+ const rows = [];
401
+ for (let row = 0; row < previewRows; row += 1) {
402
+ const values = [];
403
+ for (let col = 0; col < previewCols; col += 1) {
404
+ const offset = address + (row * dimensions[1] + col) * elementStride;
405
+ if (variable.elementKind === 'bool') {
406
+ values.push(this.mem.read8(offset) ? 'true' : 'false');
407
+ continue;
408
+ }
409
+ if (variable.elementKind === 'char') {
410
+ const charCode = this.mem.read8(offset);
411
+ values.push(charCode >= 0x20 && charCode <= 0x7e
412
+ ? `'${String.fromCharCode(charCode)}'`
413
+ : `${charCode}`);
414
+ continue;
415
+ }
416
+ if (variable.elementKind === 'float') {
417
+ values.push(`${this.mem.readFloat32(offset)}`);
418
+ continue;
419
+ }
420
+ if (variable.elementKind === 'double') {
421
+ values.push(`${this.mem.readFloat64(offset)}`);
422
+ continue;
423
+ }
424
+ values.push(`${this.mem.readInt32(offset)}`);
425
+ }
426
+ rows.push(`[${values.join(', ')}${dimensions[1] > previewCols ? ', ...' : ''}]`);
427
+ }
428
+ return [
429
+ {
430
+ name: variable.name,
431
+ value: `[${rows.join(', ')}${dimensions[0] > previewRows ? ', ...' : ''}]`
432
+ }
433
+ ];
434
+ }
435
+ const previewLength = Math.min(dimensions[0], 8);
436
+ const values = [];
437
+ for (let index = 0; index < previewLength; index += 1) {
438
+ const offset = address + index * elementStride;
439
+ if (variable.elementKind === 'bool') {
440
+ values.push(this.mem.read8(offset) ? 'true' : 'false');
441
+ continue;
442
+ }
443
+ if (variable.elementKind === 'char') {
444
+ const charCode = this.mem.read8(offset);
445
+ values.push(charCode >= 0x20 && charCode <= 0x7e
446
+ ? `'${String.fromCharCode(charCode)}'`
447
+ : `${charCode}`);
448
+ continue;
449
+ }
450
+ if (variable.elementKind === 'float') {
451
+ values.push(`${this.mem.readFloat32(offset)}`);
452
+ continue;
453
+ }
454
+ if (variable.elementKind === 'double') {
455
+ values.push(`${this.mem.readFloat64(offset)}`);
456
+ continue;
457
+ }
458
+ values.push(`${this.mem.readInt32(offset)}`);
459
+ }
460
+ return [
461
+ {
462
+ name: variable.name,
463
+ value: `[${values.join(', ')}${dimensions[0] > previewLength ? ', ...' : ''}]`
464
+ }
465
+ ];
466
+ }
179
467
  const value = session.globalValues?.get(variable.slot) ?? '?';
180
468
  return [{ name: variable.name, value }];
181
469
  }) || [];
@@ -184,9 +472,10 @@ export default class App {
184
472
  line,
185
473
  reason,
186
474
  locals: [...locals, ...globals],
187
- callStack: [...session.frames]
188
- .reverse()
189
- .map((stackFrame) => ({ functionName: stackFrame.functionName, line: stackFrame.line }))
475
+ callStack: [...session.frames].reverse().map((stackFrame) => ({
476
+ functionName: stackFrame.functionName,
477
+ line: stackFrame.line
478
+ }))
190
479
  });
191
480
  const sequence = Atomics.load(buffer, 0);
192
481
  while (true) {
@@ -226,6 +515,18 @@ export default class App {
226
515
  session.resumeSkipLine = session.currentLine;
227
516
  return ESUCCESS;
228
517
  }
518
+ if (command === 5) {
519
+ const expression = session.watchBuffer ? readBufferedStdin(session.watchBuffer) : '';
520
+ let result = '?';
521
+ try {
522
+ result = expression ? this.debugEvaluate(expression) : '?';
523
+ }
524
+ catch (error) {
525
+ result = error instanceof Error && error.message === 'unavailable' ? '?' : 'error';
526
+ }
527
+ if (session.watchResultBuffer)
528
+ flushQueuedStdin([result], session.watchResultBuffer);
529
+ }
229
530
  }
230
531
  }
231
532
  __wasm_idle_debug_enter(functionId, line) {
@@ -344,6 +645,18 @@ export default class App {
344
645
  const session = this.debugSession;
345
646
  if (!session?.buffer)
346
647
  return ESUCCESS;
648
+ const sharedBreakpointVersion = Atomics.load(session.buffer, 2);
649
+ if (sharedBreakpointVersion !== session.breakpointVersion) {
650
+ const count = Math.max(0, Atomics.load(session.buffer, 3));
651
+ const nextBreakpoints = new Set();
652
+ for (let index = 0; index < count && index + 4 < session.buffer.length; index += 1) {
653
+ const breakpoint = Atomics.load(session.buffer, index + 4);
654
+ if (breakpoint > 0)
655
+ nextBreakpoints.add(breakpoint);
656
+ }
657
+ session.breakpoints = nextBreakpoints;
658
+ session.breakpointVersion = sharedBreakpointVersion;
659
+ }
347
660
  if (session.resumeSkipActive) {
348
661
  if (functionId === session.resumeSkipFunctionId && line === session.resumeSkipLine) {
349
662
  return ESUCCESS;
@@ -380,19 +693,20 @@ export default class App {
380
693
  // +2 to account for = and \0 in "name=value\0".
381
694
  size += name.length + value.length + 2;
382
695
  }
383
- this.mem.write64(environ_count_out, names.length);
384
- this.mem.write64(environ_buf_size_out, size);
696
+ this.mem.write32(environ_count_out, names.length);
697
+ this.mem.write32(environ_buf_size_out, size);
698
+ this.trace(`environ_sizes_get(count=${names.length}, bytes=${size})`);
385
699
  return ESUCCESS;
386
700
  }
387
701
  environ_get(environ_ptrs, environ_buf) {
388
702
  this.mem.check();
389
703
  const names = Object.getOwnPropertyNames(this.environ);
704
+ this.trace(`environ_get(entries=${JSON.stringify(names)})`);
390
705
  for (const name of names) {
391
706
  this.mem.write32(environ_ptrs, environ_buf);
392
707
  environ_ptrs += 4;
393
708
  environ_buf += this.mem.writeStr(environ_buf, `${name}=${this.environ[name]}`);
394
709
  }
395
- this.mem.write32(environ_ptrs, 0);
396
710
  return ESUCCESS;
397
711
  }
398
712
  args_sizes_get(argc_out, argv_buf_size_out) {
@@ -401,18 +715,19 @@ export default class App {
401
715
  for (let arg of this.argv) {
402
716
  size += arg.length + 1; // "arg\0".
403
717
  }
404
- this.mem.write64(argc_out, this.argv.length);
405
- this.mem.write64(argv_buf_size_out, size);
718
+ this.mem.write32(argc_out, this.argv.length);
719
+ this.mem.write32(argv_buf_size_out, size);
720
+ this.trace(`args_sizes_get(count=${this.argv.length}, bytes=${size})`);
406
721
  return ESUCCESS;
407
722
  }
408
723
  args_get(argv_ptrs, argv_buf) {
409
724
  this.mem.check();
725
+ this.trace(`args_get(argv=${JSON.stringify(this.argv)})`);
410
726
  for (let arg of this.argv) {
411
727
  this.mem.write32(argv_ptrs, argv_buf);
412
728
  argv_ptrs += 4;
413
729
  argv_buf += this.mem.writeStr(argv_buf, arg);
414
730
  }
415
- this.mem.write32(argv_ptrs, 0);
416
731
  return ESUCCESS;
417
732
  }
418
733
  random_get(buf, buf_len) {
@@ -2,6 +2,7 @@ import type { DebugVariable, DebugVariableMetadata } from '../playground/options
2
2
  import App from './app';
3
3
  import { MemFS } from './memory';
4
4
  import { type Writable } from 'svelte/store';
5
+ type ClangSourceLanguage = 'C' | 'CPP';
5
6
  interface APIOption {
6
7
  stdin: () => string;
7
8
  stdout: (str: string) => void;
@@ -17,12 +18,19 @@ interface APIOption {
17
18
  path: string;
18
19
  }
19
20
  interface DebugRunOptions {
21
+ language?: ClangSourceLanguage;
20
22
  args?: string[];
23
+ compileArgs?: string[];
24
+ programArgs?: string[];
25
+ cppVersion?: string;
26
+ cVersion?: string;
21
27
  debug?: boolean;
22
28
  breakpoints?: number[];
23
29
  pauseOnEntry?: boolean;
24
30
  debugBuffer?: Int32Array;
25
31
  interruptBuffer?: Uint8Array;
32
+ watchBuffer?: Int32Array;
33
+ watchResultBuffer?: Int32Array;
26
34
  }
27
35
  export default class Clang {
28
36
  ready: Promise<void>;
@@ -38,6 +46,8 @@ export default class Clang {
38
46
  debugPauseOnEntry: boolean;
39
47
  debugBuffer?: Int32Array;
40
48
  debugInterruptBuffer?: Uint8Array;
49
+ debugWatchBuffer?: Int32Array;
50
+ debugWatchResultBuffer?: Int32Array;
41
51
  onDebugEvent?: (event: {
42
52
  type: 'pause';
43
53
  line: number;