wasm-idle 0.7.0 → 0.7.1

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.
@@ -1,7 +1,7 @@
1
1
  import { CloseAction, ErrorAction, MonacoLanguageClient, MonacoServices } from '@hancomac/monaco-languageclient';
2
- import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-jsonrpc/browser';
3
2
  import { CLANGD_CPP_FILE_URI, CLANGD_WORKSPACE_URI, normalizeClangdBaseUrl } from './config';
4
3
  import ClangdWorker from './worker?worker';
4
+ import { BrowserMessageReader, BrowserMessageWriter } from '../utils/vscodeJsonrpcBrowser';
5
5
  let servicesInstalled = false;
6
6
  export class ClangdSession {
7
7
  Monaco;
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="WebWorker" />
2
2
  import { CLANGD_CPP_FILE_PATH, CLANGD_WORKSPACE_PATH, createClangdCompileFlags, normalizeClangdBaseUrl } from './config';
3
3
  import { JsonStream } from './jsonStream';
4
- import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-jsonrpc/browser';
4
+ import { BrowserMessageReader, BrowserMessageWriter } from '../utils/vscodeJsonrpcBrowser';
5
5
  const textEncoder = new TextEncoder();
6
6
  const jsonStream = new JsonStream();
7
7
  let resolveStdinReady = () => { };
@@ -1,6 +1,6 @@
1
1
  import { resolveCppLanguageServerBaseUrl } from '../runtime';
2
2
  import ClangdWorker from '../../clangd/worker?worker';
3
- import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-jsonrpc/browser';
3
+ import { BrowserMessageReader, BrowserMessageWriter } from '../../utils/vscodeJsonrpcBrowser';
4
4
  const currentUrl = () => globalThis.location?.href || '';
5
5
  async function createServer(baseUrl) {
6
6
  let resolveReady = () => { };
@@ -1,6 +1,6 @@
1
1
  import { resolvePythonLanguageServerBaseUrl } from '../runtime';
2
2
  import PythonLspWorker from './main.worker?worker';
3
- import { BrowserMessageReader, BrowserMessageWriter } from 'vscode-jsonrpc/browser';
3
+ import { BrowserMessageReader, BrowserMessageWriter } from '../../utils/vscodeJsonrpcBrowser';
4
4
  const currentUrl = () => globalThis.location?.href || '';
5
5
  async function createServer(pyodideBaseUrl) {
6
6
  let resolveReady = () => { };
@@ -1,5 +1,3 @@
1
- import atomVmWasmUrl from '../../../../node_modules/@swmansion/popcorn/dist/AtomVM.wasm?url';
2
- import initAtomVm from '../../../../node_modules/@swmansion/popcorn/dist/AtomVM.mjs';
3
1
  import { waitForBufferedStdin } from '../stdinBuffer';
4
2
  const workerDocument = {
5
3
  querySelectorAll() {
@@ -33,6 +31,130 @@ let bundleUrl = '';
33
31
  let loadedBundleUrl = '';
34
32
  let runtimePromise = null;
35
33
  let stdinBufferElixir = null;
34
+ const elixirStdinCallNames = [
35
+ 'IO.gets',
36
+ 'IO.read',
37
+ 'IO.binread',
38
+ 'IO.getn',
39
+ ':io.get_line',
40
+ ':io.get_chars'
41
+ ];
42
+ function resolveElixirRuntimeAssetUrl(assetName, bundleAssetUrl) {
43
+ const absoluteBundleUrl = new URL(bundleAssetUrl.startsWith('/') ? `http://localhost${bundleAssetUrl}` : bundleAssetUrl, globalThis.location?.href || 'http://localhost/');
44
+ return new URL(assetName, absoluteBundleUrl).toString();
45
+ }
46
+ async function resolveAtomVmInitializer(moduleAssetUrl) {
47
+ const injectedInitializer = globalThis.__wasmIdleAtomVmInit;
48
+ if (typeof injectedInitializer === 'function') {
49
+ return injectedInitializer;
50
+ }
51
+ const module = (await import(/* @vite-ignore */ moduleAssetUrl));
52
+ return module.default;
53
+ }
54
+ function skipQuotedLiteral(source, start) {
55
+ const heredoc = source.slice(start, start + 3);
56
+ if (heredoc === '"""' || heredoc === "'''") {
57
+ const end = source.indexOf(heredoc, start + 3);
58
+ return end === -1 ? source.length : end + 3;
59
+ }
60
+ const quote = source[start];
61
+ let index = start + 1;
62
+ while (index < source.length) {
63
+ if (source[index] === '\\') {
64
+ index += 2;
65
+ continue;
66
+ }
67
+ if (source[index] === quote) {
68
+ return index + 1;
69
+ }
70
+ index += 1;
71
+ }
72
+ return source.length;
73
+ }
74
+ function findClosingParen(source, openParenIndex) {
75
+ let depth = 1;
76
+ let index = openParenIndex + 1;
77
+ while (index < source.length) {
78
+ const current = source[index];
79
+ if (current === '"' || current === "'") {
80
+ index = skipQuotedLiteral(source, index);
81
+ continue;
82
+ }
83
+ if (current === '#') {
84
+ const newlineIndex = source.indexOf('\n', index);
85
+ if (newlineIndex === -1) {
86
+ return -1;
87
+ }
88
+ index = newlineIndex + 1;
89
+ continue;
90
+ }
91
+ if (current === '(') {
92
+ depth += 1;
93
+ }
94
+ else if (current === ')') {
95
+ depth -= 1;
96
+ if (depth === 0) {
97
+ return index;
98
+ }
99
+ }
100
+ index += 1;
101
+ }
102
+ return -1;
103
+ }
104
+ function splitTopLevelArgs(source) {
105
+ const parts = [];
106
+ let start = 0;
107
+ let parenDepth = 0;
108
+ let braceDepth = 0;
109
+ let bracketDepth = 0;
110
+ let index = 0;
111
+ while (index < source.length) {
112
+ const current = source[index];
113
+ if (current === '"' || current === "'") {
114
+ index = skipQuotedLiteral(source, index);
115
+ continue;
116
+ }
117
+ if (current === '#') {
118
+ const newlineIndex = source.indexOf('\n', index);
119
+ if (newlineIndex === -1)
120
+ break;
121
+ index = newlineIndex + 1;
122
+ continue;
123
+ }
124
+ if (current === '(')
125
+ parenDepth += 1;
126
+ if (current === ')')
127
+ parenDepth -= 1;
128
+ if (current === '{')
129
+ braceDepth += 1;
130
+ if (current === '}')
131
+ braceDepth -= 1;
132
+ if (current === '[')
133
+ bracketDepth += 1;
134
+ if (current === ']')
135
+ bracketDepth -= 1;
136
+ if (current === ',' && parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) {
137
+ parts.push(source.slice(start, index).trim());
138
+ start = index + 1;
139
+ }
140
+ index += 1;
141
+ }
142
+ const trailing = source.slice(start).trim();
143
+ if (trailing) {
144
+ parts.push(trailing);
145
+ }
146
+ return parts;
147
+ }
148
+ function parseLiteralCount(source) {
149
+ const normalized = source.trim().replaceAll('_', '');
150
+ if (/^\d+$/.test(normalized)) {
151
+ const parsed = Number.parseInt(normalized, 10);
152
+ if (Number.isSafeInteger(parsed)) {
153
+ return parsed;
154
+ }
155
+ }
156
+ return null;
157
+ }
36
158
  function deserialize(module, message) {
37
159
  return JSON.parse(message, (_key, value) => {
38
160
  const isRef = typeof value === 'object' &&
@@ -146,7 +268,7 @@ function configureMessagingBridge(module) {
146
268
  };
147
269
  module.call = (process, args) => originalCall(process, module.serialize(args));
148
270
  }
149
- async function startVm(avmBundle, log) {
271
+ async function startVm(avmBundle, bundleAssetUrl, log) {
150
272
  let resolveProcess = null;
151
273
  const processPromise = new Promise((resolve, reject) => {
152
274
  const timeout = setTimeout(() => {
@@ -159,6 +281,9 @@ async function startVm(avmBundle, log) {
159
281
  resolve(process);
160
282
  };
161
283
  });
284
+ const atomVmModuleUrl = resolveElixirRuntimeAssetUrl('AtomVM.mjs', bundleAssetUrl);
285
+ const atomVmWasmUrl = resolveElixirRuntimeAssetUrl('AtomVM.wasm', bundleAssetUrl);
286
+ const initAtomVm = await resolveAtomVmInitializer(atomVmModuleUrl);
162
287
  const module = (await initAtomVm({
163
288
  locateFile(path) {
164
289
  if (path === 'AtomVM.wasm') {
@@ -211,7 +336,7 @@ async function loadRuntime(nextBundleUrl, log) {
211
336
  }
212
337
  return response.arrayBuffer();
213
338
  });
214
- return await startVm(new Int8Array(bundleBuffer), log);
339
+ return await startVm(new Int8Array(bundleBuffer), nextBundleUrl, log);
215
340
  })();
216
341
  return await runtimePromise;
217
342
  }
@@ -265,24 +390,173 @@ self.onmessage = async (event) => {
265
390
  console.log(`[wasm-idle:elixir-worker] eval bytes=${code.length}`);
266
391
  }
267
392
  let evalCode = code;
268
- if (stdinBufferElixir && /\bIO\.(?:gets|read)\s*\(/.test(code)) {
269
- const chunk = waitForBufferedStdin(stdinBufferElixir, () => postMessage({ buffer: true }));
270
- const stdinLines = chunk == null ? [] : chunk.match(/[^\n]*\n|[^\n]+$/g) || [];
271
- let stdinLineOffset = 0;
272
- evalCode = code
273
- .replace(/\bIO\.gets\s*\([^)]*\)/g, () => {
274
- const line = stdinLines[stdinLineOffset++];
275
- return line === undefined ? 'nil' : JSON.stringify(line);
276
- })
277
- .replace(/\bIO\.read\s*\(\s*(?::stdio\s*,\s*)?:line\s*\)/g, () => {
278
- const line = stdinLines[stdinLineOffset++];
279
- return line === undefined ? 'nil' : JSON.stringify(line);
280
- })
281
- .replace(/\bIO\.read\s*\(\s*(?::stdio\s*,\s*)?:all\s*\)/g, () => {
282
- const remainingInput = stdinLines.slice(stdinLineOffset).join('');
283
- stdinLineOffset = stdinLines.length;
284
- return JSON.stringify(remainingInput);
285
- });
393
+ if (stdinBufferElixir && /(?:\bIO\.|\:io\.)/.test(code)) {
394
+ let bufferedInput = '';
395
+ let reachedEof = false;
396
+ const pullStdinChunk = () => {
397
+ if (reachedEof)
398
+ return false;
399
+ const nextChunk = waitForBufferedStdin(stdinBufferElixir, () => postMessage({ buffer: true }));
400
+ if (nextChunk === null) {
401
+ reachedEof = true;
402
+ return false;
403
+ }
404
+ bufferedInput += nextChunk;
405
+ return true;
406
+ };
407
+ const readLineLiteral = (eofLiteral) => {
408
+ while (true) {
409
+ const newlineIndex = bufferedInput.indexOf('\n');
410
+ if (newlineIndex !== -1) {
411
+ const line = bufferedInput.slice(0, newlineIndex + 1);
412
+ bufferedInput = bufferedInput.slice(newlineIndex + 1);
413
+ return JSON.stringify(line);
414
+ }
415
+ if (!pullStdinChunk()) {
416
+ if (!bufferedInput) {
417
+ return eofLiteral;
418
+ }
419
+ const remainder = bufferedInput;
420
+ bufferedInput = '';
421
+ return JSON.stringify(remainder);
422
+ }
423
+ }
424
+ };
425
+ const readCountLiteral = (count, eofLiteral) => {
426
+ if (count === 0) {
427
+ return JSON.stringify('');
428
+ }
429
+ while (bufferedInput.length < count && pullStdinChunk()) {
430
+ // Keep requesting chunks until the requested count or EOF is available.
431
+ }
432
+ if (!bufferedInput) {
433
+ return eofLiteral;
434
+ }
435
+ const consumed = bufferedInput.slice(0, Math.min(count, bufferedInput.length));
436
+ bufferedInput = bufferedInput.slice(consumed.length);
437
+ return JSON.stringify(consumed);
438
+ };
439
+ const readAllLiteral = (eofLiteral) => {
440
+ while (pullStdinChunk()) {
441
+ // Drain stdin until EOF for :all reads.
442
+ }
443
+ if (!bufferedInput) {
444
+ return eofLiteral;
445
+ }
446
+ const consumed = bufferedInput;
447
+ bufferedInput = '';
448
+ return JSON.stringify(consumed);
449
+ };
450
+ const wrapWithDeviceEvaluation = (deviceArg, valueExpr) => deviceArg
451
+ ? `((fn __wasm_idle_device__ -> _ = __wasm_idle_device__; ${valueExpr} end).(${deviceArg}))`
452
+ : valueExpr;
453
+ const wrapWithPromptOutput = (promptArg, valueExpr, deviceArg) => deviceArg
454
+ ? `((fn __wasm_idle_device__, __wasm_idle_prompt__ -> _ = __wasm_idle_device__; IO.write(__wasm_idle_prompt__); ${valueExpr} end).(${deviceArg}, ${promptArg}))`
455
+ : `((fn __wasm_idle_prompt__ -> IO.write(__wasm_idle_prompt__); ${valueExpr} end).(${promptArg}))`;
456
+ let rewritten = '';
457
+ let cursor = 0;
458
+ while (cursor < code.length) {
459
+ const current = code[cursor];
460
+ if (current === '"' || current === "'") {
461
+ const nextCursor = skipQuotedLiteral(code, cursor);
462
+ rewritten += code.slice(cursor, nextCursor);
463
+ cursor = nextCursor;
464
+ continue;
465
+ }
466
+ if (current === '#') {
467
+ const newlineIndex = code.indexOf('\n', cursor);
468
+ if (newlineIndex === -1) {
469
+ rewritten += code.slice(cursor);
470
+ break;
471
+ }
472
+ rewritten += code.slice(cursor, newlineIndex + 1);
473
+ cursor = newlineIndex + 1;
474
+ continue;
475
+ }
476
+ const callName = elixirStdinCallNames.find((candidate) => {
477
+ if (!code.startsWith(candidate, cursor)) {
478
+ return false;
479
+ }
480
+ const previous = code[cursor - 1] || '';
481
+ return !/[A-Za-z0-9_:.?!]/.test(previous);
482
+ });
483
+ if (!callName) {
484
+ rewritten += current;
485
+ cursor += 1;
486
+ continue;
487
+ }
488
+ let openParenIndex = cursor + callName.length;
489
+ while (/\s/.test(code[openParenIndex] || '')) {
490
+ openParenIndex += 1;
491
+ }
492
+ if (code[openParenIndex] !== '(') {
493
+ rewritten += current;
494
+ cursor += 1;
495
+ continue;
496
+ }
497
+ const closeParenIndex = findClosingParen(code, openParenIndex);
498
+ if (closeParenIndex === -1) {
499
+ rewritten += code.slice(cursor);
500
+ break;
501
+ }
502
+ const originalCall = code.slice(cursor, closeParenIndex + 1);
503
+ const args = splitTopLevelArgs(code.slice(openParenIndex + 1, closeParenIndex));
504
+ let replacement = originalCall;
505
+ if (callName === 'IO.gets' && (args.length === 1 || args.length === 2)) {
506
+ replacement =
507
+ args.length === 2
508
+ ? wrapWithPromptOutput(args[1], readLineLiteral('nil'), args[0])
509
+ : wrapWithPromptOutput(args[0], readLineLiteral('nil'));
510
+ }
511
+ else if ((callName === 'IO.read' || callName === 'IO.binread') &&
512
+ (args.length === 1 || args.length === 2)) {
513
+ const deviceArg = args.length === 2 ? args[0] : undefined;
514
+ const modeArg = args[args.length - 1];
515
+ if (modeArg === ':line') {
516
+ replacement = wrapWithDeviceEvaluation(deviceArg, readLineLiteral(':eof'));
517
+ }
518
+ else if (modeArg === ':all') {
519
+ replacement = wrapWithDeviceEvaluation(deviceArg, readAllLiteral(':eof'));
520
+ }
521
+ else {
522
+ const count = parseLiteralCount(modeArg);
523
+ if (count === null) {
524
+ throw new Error(`${callName} stdin bridge requires a literal non-negative count`);
525
+ }
526
+ replacement = wrapWithDeviceEvaluation(deviceArg, readCountLiteral(count, ':eof'));
527
+ }
528
+ }
529
+ else if (callName === 'IO.getn' && (args.length === 2 || args.length === 3)) {
530
+ const count = parseLiteralCount(args[args.length - 1]);
531
+ if (count === null) {
532
+ throw new Error('IO.getn stdin bridge requires a literal non-negative count');
533
+ }
534
+ replacement =
535
+ args.length === 3
536
+ ? wrapWithPromptOutput(args[1], readCountLiteral(count, ':eof'), args[0])
537
+ : wrapWithPromptOutput(args[0], readCountLiteral(count, ':eof'));
538
+ }
539
+ else if (callName === ':io.get_line' && (args.length === 1 || args.length === 2)) {
540
+ replacement =
541
+ args.length === 2
542
+ ? wrapWithPromptOutput(args[1], readLineLiteral(':eof'), args[0])
543
+ : wrapWithPromptOutput(args[0], readLineLiteral(':eof'));
544
+ }
545
+ else if (callName === ':io.get_chars' &&
546
+ (args.length === 2 || args.length === 3)) {
547
+ const count = parseLiteralCount(args[args.length - 1]);
548
+ if (count === null) {
549
+ throw new Error(':io.get_chars stdin bridge requires a literal non-negative count');
550
+ }
551
+ replacement =
552
+ args.length === 3
553
+ ? wrapWithPromptOutput(args[1], readCountLiteral(count, ':eof'), args[0])
554
+ : wrapWithPromptOutput(args[0], readCountLiteral(count, ':eof'));
555
+ }
556
+ rewritten += replacement;
557
+ cursor = closeParenIndex + 1;
558
+ }
559
+ evalCode = rewritten;
286
560
  }
287
561
  const response = await runtime.module.call(runtime.process, ['eval_elixir', evalCode]);
288
562
  const rendered = runtime.module.deserialize(response);
@@ -99,17 +99,20 @@ self.onmessage = async (event) => {
99
99
  exitCode = wasiRuntime.start(instance);
100
100
  }
101
101
  else if (typeof exportsObject.memory === 'object' &&
102
- typeof exportsObject.main === 'function') {
102
+ (typeof exportsObject.main === 'function' ||
103
+ typeof exportsObject._initialize === 'function')) {
103
104
  wasiRuntime.initialize(instance);
104
105
  if (typeof exportsObject._initialize !== 'function' &&
105
106
  typeof exportsObject.__wasm_call_ctors === 'function') {
106
107
  exportsObject.__wasm_call_ctors();
107
108
  }
108
- const mainResult = exportsObject.main();
109
- exitCode = typeof mainResult === 'number' ? mainResult : 0;
109
+ if (typeof exportsObject.main === 'function') {
110
+ const mainResult = exportsObject.main();
111
+ exitCode = typeof mainResult === 'number' ? mainResult : 0;
112
+ }
110
113
  }
111
114
  else {
112
- throw new Error('TinyGo worker expected a WASI artifact with _start or a reactor artifact with main');
115
+ throw new Error('TinyGo worker expected a WASI artifact with _start or a reactor artifact with _initialize or main');
113
116
  }
114
117
  }
115
118
  catch (error) {
@@ -0,0 +1,5 @@
1
+ import type { BrowserMessageReader as BrowserMessageReaderInstance, BrowserMessageWriter as BrowserMessageWriterInstance } from 'vscode-jsonrpc/lib/browser/main';
2
+ export declare const BrowserMessageReader: new (port: MessagePort | Worker | DedicatedWorkerGlobalScope) => BrowserMessageReaderInstance;
3
+ export declare const BrowserMessageWriter: new (port: MessagePort | Worker | DedicatedWorkerGlobalScope) => BrowserMessageWriterInstance;
4
+ export type BrowserMessageReader = BrowserMessageReaderInstance;
5
+ export type BrowserMessageWriter = BrowserMessageWriterInstance;
@@ -0,0 +1,4 @@
1
+ import jsonrpc from 'vscode-jsonrpc';
2
+ const browserJsonrpc = jsonrpc;
3
+ export const BrowserMessageReader = browserJsonrpc.BrowserMessageReader;
4
+ export const BrowserMessageWriter = browserJsonrpc.BrowserMessageWriter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wasm-idle",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "A simple web app to run code in the browser",
5
5
  "license": "MIT",
6
6
  "type": "module",