tree-sitter.tree-sitter/test/fixtures/rust_wasm_web/run.mjs
Max Brunsfeld 0e2af0d8d1
Allow using Tree-sitter rust lib in multi-threaded web apps compiled to wasm32-unknown-unknown (#5851)
* feat(wasm): make syntax trees sendable

* test(wasm): transfer trees across workers

* test(wasm): use JSON grammar for tree transfer

* test(wasm): edit trees across workers

* test(wasm): share dlmalloc with tree-sitter

* test(wasm): simplify worker tree exchange

* test(wasm): drive tree exchange from Rust

* test(wasm): split sendable-tree xtask

* test(wasm): generalize Rust web fixture

* test(wasm): exercise parallel Rust tree access

* fix(wasm): use Rust global allocator for C core

* test(wasm): use default Rust allocator

* feat(wasm): support external scanners in Rust web apps

* Simplify example further, add a readme

* Regenerate wasm-stdlib

* Fix wasm_stdlib check script

* Vendor the Wasm standard library subset

* Test Unicode Ruby scanner behavior in Wasm

* Make Wasm tree languages instance-aware

* Test multi-threaded use of queries in wasm32-unknown

* Refactor reference-counted language storage

* Check ABI version compat before loading rest of language

* 🎨 Remove redundant #ifdef block

* Reject unsupported Rust Wasm builds on 0.26
2026-08-14 17:05:42 -07:00

103 lines
3 KiB
JavaScript

import fs from 'node:fs/promises';
import { Worker } from 'node:worker_threads';
const MEMORY_PAGES = 256;
const MAX_MEMORY_PAGES = 4096;
const control = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
const [runtimePath, pythonPath, rubyPath] = process.argv.slice(2);
if (!runtimePath || !pythonPath || !rubyPath) {
throw new Error('usage: node run.mjs <runtime.wasm> <python.wasm> <ruby.wasm>');
}
const runtimeModule = await WebAssembly.compile(await fs.readFile(runtimePath));
const languageModules = await Promise.all(
[pythonPath, rubyPath].map(async path => WebAssembly.compile(await fs.readFile(path))),
);
const memory = new WebAssembly.Memory({
initial: MEMORY_PAGES,
maximum: MAX_MEMORY_PAGES,
shared: true,
});
if (!(memory.buffer instanceof SharedArrayBuffer)) {
throw new Error('WebAssembly memory is not shared');
}
let worker;
let pendingRequest = 0;
const uiRuntime = await WebAssembly.instantiate(runtimeModule, {
env: {
memory,
request_parse(languageId, sourceAddress, sourceLength, oldTree, requestAddress) {
if (pendingRequest !== 0) {
throw new Error('Rust requested another parse before the prior request completed');
}
pendingRequest = requestAddress;
if (oldTree) {
Atomics.store(control, 0, 0);
}
worker.postMessage(
oldTree
? { languageId, sourceAddress, sourceLength, oldTree }
: { languageId, sourceAddress, sourceLength },
);
},
log(messageAddress, messageLength) {
console.log(
new TextDecoder().decode(
new Uint8Array(memory.buffer, messageAddress, messageLength),
),
);
},
notify_parsing_started() {
throw new Error('UI runtime unexpectedly paused for parsing');
},
},
});
worker = new Worker(new URL('./worker.mjs', import.meta.url), {
workerData: { control, runtimeModule, languageModules, memory },
});
try {
await new Promise((resolve, reject) => {
let finished = false;
worker.once('error', reject);
worker.once('exit', code => {
if (!finished) {
reject(new Error(`worker exited before completing the test with code ${code}`));
}
});
worker.on('message', message => {
try {
if (message?.ready === true) {
uiRuntime.exports.start();
return;
}
if (message?.parsing) {
console.log('parsing...');
Atomics.store(control, 0, 1);
Atomics.notify(control, 0);
return;
}
if (!Number.isInteger(message?.tree) || message.tree === 0) {
throw new Error('worker did not return a tree');
}
const request = pendingRequest;
if (request === 0) {
throw new Error('worker returned a tree without a pending request');
}
pendingRequest = 0;
if (uiRuntime.exports.tree_ready(request, message.tree)) {
finished = true;
resolve();
}
} catch (error) {
reject(error);
}
});
});
} finally {
await worker.terminate();
}