wasm-idle 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,7 @@ interface DebugSession {
18
18
  nextLineFunctionId: number;
19
19
  nextLineLine: number;
20
20
  variableMetadata: Record<number, DebugVariableMetadata[]>;
21
+ globalVariableMetadata: DebugVariableMetadata[];
21
22
  functionMetadata: Record<number, string>;
22
23
  frames: Array<{
23
24
  functionId: number;
@@ -25,6 +26,7 @@ interface DebugSession {
25
26
  line: number;
26
27
  values: Map<number, string>;
27
28
  }>;
29
+ globalValues: Map<number, string>;
28
30
  onPause?: (event: {
29
31
  type: 'pause';
30
32
  line: number;
package/dist/clang/app.js CHANGED
@@ -103,6 +103,10 @@ export default class App {
103
103
  const session = this.debugSession;
104
104
  if (!session?.buffer)
105
105
  return ESUCCESS;
106
+ if (functionId === 0) {
107
+ session.globalValues.set(slot, Number.isInteger(value) ? String(value) : `${value}`);
108
+ return ESUCCESS;
109
+ }
106
110
  for (let index = session.frames.length - 1; index >= 0; index -= 1) {
107
111
  const frame = session.frames[index];
108
112
  if (frame?.functionId !== functionId)
@@ -116,6 +120,10 @@ export default class App {
116
120
  const session = this.debugSession;
117
121
  if (!session?.buffer)
118
122
  return ESUCCESS;
123
+ if (functionId === 0) {
124
+ session.globalValues.set(slot, value ? 'true' : 'false');
125
+ return ESUCCESS;
126
+ }
119
127
  for (let index = session.frames.length - 1; index >= 0; index -= 1) {
120
128
  const frame = session.frames[index];
121
129
  if (frame?.functionId !== functionId)
@@ -129,6 +137,10 @@ export default class App {
129
137
  const session = this.debugSession;
130
138
  if (!session?.buffer)
131
139
  return ESUCCESS;
140
+ if (functionId === 0) {
141
+ session.globalValues.set(slot, String(value >>> 0));
142
+ return ESUCCESS;
143
+ }
132
144
  for (let index = session.frames.length - 1; index >= 0; index -= 1) {
133
145
  const frame = session.frames[index];
134
146
  if (frame?.functionId !== functionId)
@@ -144,6 +156,10 @@ export default class App {
144
156
  return ESUCCESS;
145
157
  this.mem?.check?.();
146
158
  const text = this.mem?.readStr ? this.mem.readStr(ptr, len) : '?';
159
+ if (functionId === 0) {
160
+ session.globalValues.set(slot, text);
161
+ return ESUCCESS;
162
+ }
147
163
  for (let index = session.frames.length - 1; index >= 0; index -= 1) {
148
164
  const frame = session.frames[index];
149
165
  if (frame?.functionId !== functionId)
@@ -198,18 +214,59 @@ export default class App {
198
214
  if (variable.kind === 'array') {
199
215
  this.mem?.check?.();
200
216
  const address = Number(frame?.values.get(variable.slot) ?? Number.NaN);
201
- if (!Number.isFinite(address) || address <= 0 || !variable.length || !variable.elementKind) {
217
+ const dimensions = variable.dimensions?.length
218
+ ? variable.dimensions
219
+ : variable.length
220
+ ? [variable.length]
221
+ : [];
222
+ if (!Number.isFinite(address) || address <= 0 || !dimensions.length || !variable.elementKind) {
202
223
  return [{ name: variable.name, value: '?' }];
203
224
  }
204
- const previewLength = Math.min(variable.length, 8);
225
+ const elementStride = variable.elementKind === 'double'
226
+ ? 8
227
+ : variable.elementKind === 'bool' || variable.elementKind === 'char'
228
+ ? 1
229
+ : 4;
230
+ if (dimensions.length === 2) {
231
+ const previewRows = Math.min(dimensions[0], 4);
232
+ const previewCols = Math.min(dimensions[1], 8);
233
+ const rows = [];
234
+ for (let row = 0; row < previewRows; row += 1) {
235
+ const values = [];
236
+ for (let col = 0; col < previewCols; col += 1) {
237
+ const offset = address + (row * dimensions[1] + col) * elementStride;
238
+ if (variable.elementKind === 'bool') {
239
+ values.push(this.mem.read8(offset) ? 'true' : 'false');
240
+ continue;
241
+ }
242
+ if (variable.elementKind === 'char') {
243
+ const charCode = this.mem.read8(offset);
244
+ values.push(charCode >= 0x20 && charCode <= 0x7e ? `'${String.fromCharCode(charCode)}'` : `${charCode}`);
245
+ continue;
246
+ }
247
+ if (variable.elementKind === 'float') {
248
+ values.push(`${this.mem.readFloat32(offset)}`);
249
+ continue;
250
+ }
251
+ if (variable.elementKind === 'double') {
252
+ values.push(`${this.mem.readFloat64(offset)}`);
253
+ continue;
254
+ }
255
+ values.push(`${this.mem.readInt32(offset)}`);
256
+ }
257
+ rows.push(`[${values.join(', ')}${dimensions[1] > previewCols ? ', ...' : ''}]`);
258
+ }
259
+ return [
260
+ {
261
+ name: variable.name,
262
+ value: `[${rows.join(', ')}${dimensions[0] > previewRows ? ', ...' : ''}]`
263
+ }
264
+ ];
265
+ }
266
+ const previewLength = Math.min(dimensions[0], 8);
205
267
  const values = [];
206
268
  for (let index = 0; index < previewLength; index += 1) {
207
- const offset = address +
208
- (variable.elementKind === 'double'
209
- ? index * 8
210
- : variable.elementKind === 'bool' || variable.elementKind === 'char'
211
- ? index
212
- : index * 4);
269
+ const offset = address + index * elementStride;
213
270
  if (variable.elementKind === 'bool') {
214
271
  values.push(this.mem.read8(offset) ? 'true' : 'false');
215
272
  continue;
@@ -232,18 +289,27 @@ export default class App {
232
289
  return [
233
290
  {
234
291
  name: variable.name,
235
- value: `[${values.join(', ')}${variable.length > previewLength ? ', ...' : ''}]`
292
+ value: `[${values.join(', ')}${dimensions[0] > previewLength ? ', ...' : ''}]`
236
293
  }
237
294
  ];
238
295
  }
239
296
  const value = frame?.values.get(variable.slot) ?? '?';
240
297
  return [{ name: variable.name, value }];
241
298
  }) || [];
299
+ const localNames = new Set(locals.map((variable) => variable.name));
300
+ const globals = (session.globalVariableMetadata || []).flatMap((variable) => {
301
+ if (localNames.has(variable.name))
302
+ return [];
303
+ if (line < variable.fromLine || line > variable.toLine)
304
+ return [];
305
+ const value = session.globalValues?.get(variable.slot) ?? '?';
306
+ return [{ name: variable.name, value }];
307
+ }) || [];
242
308
  session.onPause?.({
243
309
  type: 'pause',
244
310
  line,
245
311
  reason,
246
- locals,
312
+ locals: [...locals, ...globals],
247
313
  callStack: [...session.frames]
248
314
  .reverse()
249
315
  .map((stackFrame) => ({ functionName: stackFrame.functionName, line: stackFrame.line }))
@@ -49,6 +49,7 @@ export default class Clang {
49
49
  }[];
50
50
  }) => void;
51
51
  debugVariableMetadata: Record<number, DebugVariableMetadata[]>;
52
+ debugGlobalMetadata: DebugVariableMetadata[];
52
53
  debugFunctionMetadata: Record<number, string>;
53
54
  lastBuildKey: string;
54
55
  path: string;
@@ -52,6 +52,7 @@ export default class Clang {
52
52
  debugInterruptBuffer;
53
53
  onDebugEvent;
54
54
  debugVariableMetadata = {};
55
+ debugGlobalMetadata = [];
55
56
  debugFunctionMetadata = {};
56
57
  lastBuildKey = '';
57
58
  path;
@@ -136,11 +137,14 @@ export default class Clang {
136
137
  let nextFunctionId = 1;
137
138
  let nextVariableSlot = 1;
138
139
  let currentFunctionVariables = new Map();
140
+ let globalVariables = new Map();
139
141
  let currentFunctionContainers = new Map();
140
142
  let inBlockComment = false;
141
143
  let pendingFunctionHeader;
142
144
  this.debugVariableMetadata = {};
145
+ this.debugGlobalMetadata = [];
143
146
  this.debugFunctionMetadata = {};
147
+ const globalInitialization = [];
144
148
  const instrumented = [
145
149
  '#include <map>',
146
150
  '#include <set>',
@@ -235,10 +239,64 @@ export default class Clang {
235
239
  analysisLine = analysisLine.slice(0, lineComment);
236
240
  const normalized = analysisLine.trim();
237
241
  const inFunctionBody = functionDepth > 0 && braceDepth >= functionDepth;
242
+ const isTopLevelDeclarationContext = functionDepth === 0 &&
243
+ braceDepth === 0 &&
244
+ !normalized.includes('(') &&
245
+ !normalized.startsWith('#');
238
246
  const controlHeaderWithoutInlineBlock = /^(while|if|for)\s*\(/.test(normalized) && !normalized.includes('{');
239
247
  const leadingInstrumentation = [];
240
248
  const trailingInstrumentation = [];
241
249
  const declaredContainerNames = new Set();
250
+ const globalDeclarationMatch = isTopLevelDeclarationContext &&
251
+ normalized.match(/^(?:const\s+)?(?:(?:unsigned|signed)\s+)?(?:(?:short|long long|long)\s+)?(int|float|double|bool|char)\s+(.+);$/);
252
+ if (globalDeclarationMatch) {
253
+ const kind = globalDeclarationMatch[1] === 'bool' ? 'bool' : 'number';
254
+ const declarations = [];
255
+ let declarationBuffer = '';
256
+ let declarationBraceDepth = 0;
257
+ for (const character of globalDeclarationMatch[2]) {
258
+ if (character === ',' && declarationBraceDepth === 0) {
259
+ if (declarationBuffer.trim())
260
+ declarations.push(declarationBuffer.trim());
261
+ declarationBuffer = '';
262
+ continue;
263
+ }
264
+ if (character === '{')
265
+ declarationBraceDepth += 1;
266
+ if (character === '}')
267
+ declarationBraceDepth = Math.max(0, declarationBraceDepth - 1);
268
+ declarationBuffer += character;
269
+ }
270
+ if (declarationBuffer.trim())
271
+ declarations.push(declarationBuffer.trim());
272
+ for (const declaration of declarations) {
273
+ const [left] = declaration.split('=');
274
+ const declarator = left?.trim() || '';
275
+ if (/[*&\[]/.test(declarator))
276
+ continue;
277
+ const name = declarator.match(/([A-Za-z_]\w*)\s*$/)?.[1];
278
+ if (!name)
279
+ continue;
280
+ const slot = nextVariableSlot++;
281
+ globalVariables.set(name, {
282
+ slot,
283
+ kind,
284
+ fromLine: index + 1,
285
+ toLine: Number.MAX_SAFE_INTEGER
286
+ });
287
+ this.debugGlobalMetadata = [
288
+ ...this.debugGlobalMetadata,
289
+ {
290
+ slot,
291
+ name,
292
+ kind,
293
+ fromLine: index + 1,
294
+ toLine: Number.MAX_SAFE_INTEGER
295
+ }
296
+ ];
297
+ globalInitialization.push(`${kind === 'bool' ? '__wasm_idle_debug_value_bool' : '__wasm_idle_debug_value_num'}(0, ${slot}, ${name});`);
298
+ }
299
+ }
242
300
  if (inFunctionBody &&
243
301
  normalized &&
244
302
  !normalized.startsWith('#') &&
@@ -306,22 +364,27 @@ export default class Clang {
306
364
  for (const declaration of declarations) {
307
365
  const [left] = declaration.split('=');
308
366
  const declarator = left?.trim() || '';
309
- const arrayMatch = declarator.match(/([A-Za-z_]\w*)\s*\[(\d+)\]\s*$/);
310
- if (arrayMatch) {
367
+ const arrayDimensions = [];
368
+ for (const match of declarator.matchAll(/\[(\d+)\]/g)) {
369
+ arrayDimensions.push(Number(match[1]));
370
+ }
371
+ const arrayName = declarator.match(/([A-Za-z_]\w*)\s*(?=\[\d+\])/);
372
+ if (arrayDimensions.length && arrayName) {
311
373
  const slot = nextVariableSlot++;
312
374
  this.debugVariableMetadata[currentFunctionId] = [
313
375
  ...(this.debugVariableMetadata[currentFunctionId] || []),
314
376
  {
315
377
  slot,
316
- name: arrayMatch[1],
378
+ name: arrayName[1],
317
379
  kind: 'array',
318
380
  elementKind: declarationMatch[1],
319
- length: Number(arrayMatch[2]),
381
+ length: arrayDimensions[0],
382
+ dimensions: arrayDimensions,
320
383
  fromLine: index + 1,
321
384
  toLine: Number.MAX_SAFE_INTEGER
322
385
  }
323
386
  ];
324
- trailingInstrumentation.push(`${indent}__wasm_idle_debug_value_addr(${currentFunctionId}, ${slot}, (int)((unsigned long long)(${arrayMatch[1]})));`);
387
+ trailingInstrumentation.push(`${indent}__wasm_idle_debug_value_addr(${currentFunctionId}, ${slot}, (int)((unsigned long long)(${arrayName[1]})));`);
325
388
  continue;
326
389
  }
327
390
  if (/[*&]/.test(declarator))
@@ -427,6 +490,16 @@ export default class Clang {
427
490
  break;
428
491
  }
429
492
  }
493
+ for (const [name, variable] of globalVariables) {
494
+ if (currentFunctionVariables.has(name) || currentFunctionContainers.has(name))
495
+ continue;
496
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
497
+ if (new RegExp(`(?:^|[^\\w])(?:\\+\\+|--)\\s*${escapedName}\\b`).test(normalized) ||
498
+ new RegExp(`\\b${escapedName}\\s*(?:[+\\-*/%]?=|\\+\\+|--)`).test(normalized) ||
499
+ new RegExp(`&\\s*${escapedName}\\b`).test(normalized)) {
500
+ trailingInstrumentation.push(`${indent}${variable.kind === 'bool' ? '__wasm_idle_debug_value_bool' : '__wasm_idle_debug_value_num'}(0, ${variable.slot}, ${name});`);
501
+ }
502
+ }
430
503
  }
431
504
  if (closeIndex > openIndex) {
432
505
  const conditionSource = line.slice(openIndex + 1, closeIndex);
@@ -544,22 +617,29 @@ export default class Clang {
544
617
  instrumented.push(`${indent} __wasm_idle_debug_emit_${container}(${currentFunctionId}, ${slot}, ${name});`);
545
618
  continue;
546
619
  }
547
- const parameterArrayMatch = cleaned.match(/([A-Za-z_]\w*)\s*\[(\d+)\]\s*$/);
548
- if (parameterArrayMatch && /\b(int|float|double|bool|char)\b/.test(cleaned)) {
620
+ const parameterArrayDimensions = [];
621
+ for (const match of cleaned.matchAll(/\[(\d+)\]/g)) {
622
+ parameterArrayDimensions.push(Number(match[1]));
623
+ }
624
+ const parameterArrayName = cleaned.match(/([A-Za-z_]\w*)\s*(?=\[\d+\])/);
625
+ if (parameterArrayDimensions.length &&
626
+ parameterArrayName &&
627
+ /\b(int|float|double|bool|char)\b/.test(cleaned)) {
549
628
  const slot = nextVariableSlot++;
550
629
  this.debugVariableMetadata[currentFunctionId] = [
551
630
  ...(this.debugVariableMetadata[currentFunctionId] || []),
552
631
  {
553
632
  slot,
554
- name: parameterArrayMatch[1],
633
+ name: parameterArrayName[1],
555
634
  kind: 'array',
556
635
  elementKind: (cleaned.match(/\b(int|float|double|bool|char)\b/)?.[1] || 'int'),
557
- length: Number(parameterArrayMatch[2]),
636
+ length: parameterArrayDimensions[0],
637
+ dimensions: parameterArrayDimensions,
558
638
  fromLine: index + 1,
559
639
  toLine: Number.MAX_SAFE_INTEGER
560
640
  }
561
641
  ];
562
- instrumented.push(`${indent} __wasm_idle_debug_value_addr(${currentFunctionId}, ${slot}, (int)((unsigned long long)(${parameterArrayMatch[1]})));`);
642
+ instrumented.push(`${indent} __wasm_idle_debug_value_addr(${currentFunctionId}, ${slot}, (int)((unsigned long long)(${parameterArrayName[1]})));`);
563
643
  continue;
564
644
  }
565
645
  if (/[*&\[]/.test(cleaned))
@@ -617,10 +697,18 @@ export default class Clang {
617
697
  currentFunctionContainers = new Map();
618
698
  }
619
699
  }
700
+ if (globalInitialization.length) {
701
+ instrumented.push('struct __wasm_idle_debug_globals_init {');
702
+ instrumented.push(' __wasm_idle_debug_globals_init() {');
703
+ instrumented.push(...globalInitialization.map((line) => ` ${line}`));
704
+ instrumented.push(' }');
705
+ instrumented.push('} __wasm_idle_debug_globals_init_instance;');
706
+ }
620
707
  source = instrumented.join('\n');
621
708
  }
622
709
  else {
623
710
  this.debugVariableMetadata = {};
711
+ this.debugGlobalMetadata = [];
624
712
  }
625
713
  const code = toUtf8(source);
626
714
  await this.ready;
@@ -677,8 +765,10 @@ export default class Clang {
677
765
  nextLineFunctionId: 0,
678
766
  nextLineLine: 0,
679
767
  variableMetadata: this.debugVariableMetadata,
768
+ globalVariableMetadata: this.debugGlobalMetadata,
680
769
  functionMetadata: this.debugFunctionMetadata,
681
770
  frames: [],
771
+ globalValues: new Map(),
682
772
  onPause: (event) => this.onDebugEvent?.(event)
683
773
  };
684
774
  const instantiate = +new Date();
@@ -6,6 +6,7 @@ export function evaluateDebugExpression(expression, variables) {
6
6
  const values = new Map();
7
7
  for (const variable of variables)
8
8
  values.set(variable.name, variable.value);
9
+ const parsedValues = new Map();
9
10
  const tokens = [];
10
11
  for (let index = 0; index < source.length;) {
11
12
  const character = source[index];
@@ -20,6 +21,11 @@ export function evaluateDebugExpression(expression, variables) {
20
21
  index += 1;
21
22
  continue;
22
23
  }
24
+ if (character === '[' || character === ']') {
25
+ tokens.push({ type: 'bracket', value: character });
26
+ index += 1;
27
+ continue;
28
+ }
23
29
  const operator = operators.find((candidate) => source.startsWith(candidate, index));
24
30
  if (operator) {
25
31
  tokens.push({ type: 'operator', value: operator });
@@ -46,6 +52,73 @@ export function evaluateDebugExpression(expression, variables) {
46
52
  throw new Error(`unsupported token near "${source.slice(index)}"`);
47
53
  }
48
54
  let cursor = 0;
55
+ const parsePreviewValue = (text, start = 0) => {
56
+ let index = start;
57
+ while (/\s/.test(text[index] || ''))
58
+ index += 1;
59
+ const character = text[index];
60
+ if (character === '[') {
61
+ index += 1;
62
+ const items = [];
63
+ while (true) {
64
+ while (/\s/.test(text[index] || ''))
65
+ index += 1;
66
+ if (text[index] === ']')
67
+ return { value: items, next: index + 1 };
68
+ if (text.startsWith('...', index))
69
+ throw new Error('unavailable');
70
+ const item = parsePreviewValue(text, index);
71
+ items.push(item.value);
72
+ index = item.next;
73
+ while (/\s/.test(text[index] || ''))
74
+ index += 1;
75
+ if (text[index] === ',') {
76
+ index += 1;
77
+ continue;
78
+ }
79
+ if (text[index] === ']')
80
+ return { value: items, next: index + 1 };
81
+ throw new Error('unsupported array preview');
82
+ }
83
+ }
84
+ if (character === "'") {
85
+ const end = text.indexOf("'", index + 1);
86
+ if (end === -1)
87
+ throw new Error('unsupported char preview');
88
+ return { value: text.slice(index + 1, end), next: end + 1 };
89
+ }
90
+ if (text.startsWith('true', index))
91
+ return { value: true, next: index + 4 };
92
+ if (text.startsWith('false', index))
93
+ return { value: false, next: index + 5 };
94
+ const number = text.slice(index).match(/^-?\d+(?:\.\d+)?/);
95
+ if (number?.[0])
96
+ return { value: Number(number[0]), next: index + number[0].length };
97
+ throw new Error('unsupported preview');
98
+ };
99
+ const resolveIdentifierValue = (name) => {
100
+ if (parsedValues.has(name))
101
+ return parsedValues.get(name);
102
+ const value = values.get(name);
103
+ if (value == null || value === '?')
104
+ throw new Error('unavailable');
105
+ if (value === 'true' || value === 'false') {
106
+ const parsed = value === 'true';
107
+ parsedValues.set(name, parsed);
108
+ return parsed;
109
+ }
110
+ const numeric = Number(value);
111
+ if (!Number.isNaN(numeric)) {
112
+ parsedValues.set(name, numeric);
113
+ return numeric;
114
+ }
115
+ if (value.startsWith('[')) {
116
+ const parsed = parsePreviewValue(value).value;
117
+ parsedValues.set(name, parsed);
118
+ return parsed;
119
+ }
120
+ throw new Error(`unsupported value for ${name}`);
121
+ };
49
122
  const parsePrimary = () => {
50
123
  const token = tokens[cursor];
51
124
  if (!token)
@@ -60,15 +133,22 @@ export function evaluateDebugExpression(expression, variables) {
60
133
  }
61
134
  if (token.type === 'identifier') {
62
135
  cursor += 1;
63
- const value = values.get(token.value);
64
- if (value == null || value === '?')
65
- throw new Error('unavailable');
66
- if (value === 'true' || value === 'false')
67
- return value === 'true';
68
- const parsed = Number(value);
69
- if (!Number.isNaN(parsed))
70
- return parsed;
71
- throw new Error(`unsupported value for ${token.value}`);
136
+ let resolved = resolveIdentifierValue(token.value);
137
+ while (tokens[cursor]?.type === 'bracket' && tokens[cursor]?.value === '[') {
138
+ cursor += 1;
139
+ const index = Number(parseOr());
140
+ const closing = tokens[cursor];
141
+ if (!closing || closing.type !== 'bracket' || closing.value !== ']')
142
+ throw new Error('missing closing bracket');
143
+ cursor += 1;
144
+ if (!Array.isArray(resolved) || !Number.isInteger(index))
145
+ throw new Error('unsupported index access');
146
+ const next = resolved[index];
147
+ if (next == null)
148
+ throw new Error('unavailable');
149
+ resolved = next;
150
+ }
151
+ return resolved;
72
152
  }
73
153
  if (token.type === 'paren' && token.value === '(') {
74
154
  cursor += 1;
@@ -14,6 +14,7 @@ export interface DebugVariableMetadata {
14
14
  toLine: number;
15
15
  elementKind?: DebugArrayElementKind;
16
16
  length?: number;
17
+ dimensions?: number[];
17
18
  }
18
19
  export interface DebugFrame {
19
20
  functionName: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wasm-idle",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "A simple web app to run C++/Python code in the browser",
5
5
  "license": "MIT",
6
6
  "type": "module",