// runs the language server as WebAssembly (server/lsp.wasm, built by // src/tools/mklspwasm.sh): the microcore with the server's image inside. it presents // the same surface as vmProcess: stdin to write, stdout and stderr to listen to, // an exit event and kill(). it is much faster than the javascript vm, because // the server's 64-bit words are native here and not bigints. // // the module is synchronous to load and to run, so the server starts in the // extension host's own thread. a step runs until the server waits for input, // then returns, the way vmProcess does. // // opts.log(level, text) gets what happens: the module loaded, each halt, and the // messages the server writes to its error stream. const fs = require('fs'); const { EventEmitter } = require('events'); const CHUNK = 65536; // the most a single feed can carry (wasm_chunk_cap) function wasmProcess(file, opts = {}) { const log = opts.log || (() => {}); const t0 = Date.now(); const bytes = fs.readFileSync(file); const mod = new WebAssembly.Module(bytes); log('message', 'compiled ' + file + ' (' + bytes.length + ' bytes) in ' + (Date.now() - t0) + ' ms'); const proc = new EventEmitter(); const out = new EventEmitter(); const err = new EventEmitter(); let outBytes = [], msgBytes = []; let dead = false, steps = 0, bytesIn = 0, bytesOut = 0; const inst = new WebAssembly.Instance(mod, { env: { js_putc: c => { outBytes.push(c & 255); }, js_msg: c => { msgBytes.push(c & 255); }, }, }); const x = inst.exports; if (x.wasm_chunk_cap() < CHUNK) throw new Error('lsp.wasm has a smaller input chunk than this host expects'); const flush = () => { if (outBytes.length) { const chunk = Buffer.from(outBytes); outBytes = []; bytesOut += chunk.length; out.emit('data', chunk); } if (msgBytes.length) { const text = Buffer.from(msgBytes).toString('utf8'); msgBytes = []; log('problem', 'server says: ' + text.trim().slice(0, 500)); err.emit('data', Buffer.from(text, 'utf8')); } }; const after = status => { steps++; flush(); if (status !== 3 && !dead) { // it halted: the server has exited dead = true; log(status === 0 ? 'message' : 'problem', 'the server halted with status ' + status + ' after ' + steps + ' steps, ' + bytesIn + ' bytes in, ' + bytesOut + ' out'); proc.emit('exit', status); } else if (status === 3 && x.wasm_polling && x.wasm_polling()) { // the server asked whether input is waiting: let the event loop deliver it, then go on setImmediate(() => { if (!dead && x.wasm_polling && x.wasm_polling()) after(x.wasm_resume()); }); } }; proc.stdin = { writable: true, write(buf) { if (dead) { log('problem', 'write to a halted server dropped (' + buf.length + ' bytes)'); return false; } bytesIn += buf.length; const chunkAt = x.wasm_chunk(); let off = 0; while (off < buf.length) { const n = Math.min(CHUNK, buf.length - off); new Uint8Array(x.memory.buffer).set(buf.subarray(off, off + n), chunkAt); if (x.wasm_feed(n) !== 0) { log('problem', 'the server input is full; dropped ' + (buf.length - off) + ' bytes'); return false; } off += n; } after(x.wasm_resume()); return true; }, }; proc.stdout = out; proc.stderr = err; proc.kill = () => { if (dead) return; dead = true; log('message', 'server killed after ' + steps + ' steps'); proc.emit('exit', null); }; const t1 = Date.now(); after(x.wasm_start()); log('message', 'server ran to its first wait in ' + (Date.now() - t1) + ' ms'); return proc; } module.exports = { wasmProcess };