Hey all. This has been attempted before, but I've been experimenting with creating a WIT+WAC LSP (if there is one already or if someone is already working on this lmk - I couldn't find anything).
I want it to be backed by wit-parser, but I've ran into issues with the parser returning anyhow on the API boundary, which makes it impossible to recover structured error information (like spans/locations) without doing fragile hacks.
I'd like to change some of the public methods in wit-parser to return a structured error (an enum of Lex/Parse/etc error where each variant has location info), but I'm not sure if it's better for the spans attached to those errors to be global (ie Span) and require the consumer to resolve them using SourceMap, or whether we should return file-local spans (which would make for a nicer API, but would potentially be more annoying on the wit-parser side). Any preference?
I opened a draft PR that is still a bit messy (and large, and doesn't pass all tests, and contains breaking changes) to show the direction I'd like to go in. I'm returning global spans for now because that was the easiest way to make the code compile, but I can change that. https://github.com/bytecodealliance/wasm-tools/pull/2460
Happy to discuss the API design here.
I'll read over what you have today and see what thoughts come to mind and leave some comments on the PR. Thanks for your work here though!
Returning anyhow::Error is more-or-less "easy mode" but definitely has the downsidse you mention, and the overall goal makes sense to me and I think is reasonable to implement
Speaking of wasm-tools and WIT what's wrong with this WIT? wasm-tools throws
../wasm-tools/wasm-tools component embed ./quic.wit ./quic.wasm -o quic.embedded.wasm
works.
../wasm-tools/wasm-tools component new quic.embedded.wasm -o quic.component.wasm
error: failed to encode a component from module
Caused by:
0: failed to decode world from module
1: module was not valid
2: failed to resolve import `env::console_log`
3: import interface `env` is missing function `console_log` that is required by the module
Generated by this script from WAT
// WIT Generator Script
// https://share.google/aimode/8MIUn7InsCSvw9Uxv
const fs = require('fs');
const path = require('path');
const mapT = t => ({ i32: 'u32', i64: 'u64', f32: 'float32', f64: 'float64' }[t] || 'u32');
function buildS(p, r, n) {
let raw = [];
for (let i = 0; i < p.length; i++) {
if (i + 1 < p.length && p[i] === 'i32' && p[i+1] === 'i32') {
raw.push(/log|name|path|uri/.test(n.toLowerCase()) ? 'string' : 'list<u8>');
i++;
} else raw.push(mapT(p[i]));
}
const ps = raw.length === 1 ? `argv: ${raw[0]}` : raw.map((t, i) => `argv${i}: ${t}`).join(', ');
const res = r.map(mapT);
const rs = res.length === 1 ? ` -> ${res[0]}` : res.length > 1 ? ` -> tuple<${res.join(', ')}>` : '';
return `func(${ps})${rs}`;
}
function watToWit(p) {
const c = fs.readFileSync(p, 'utf8');
const base = path.basename(p, path.extname(p)).replace(/_/g, '-').toLowerCase();
const types = {};
[...c.matchAll(/\(type\s+\(;(\d+);\)\s+\(func\s+((?:\(param\s+[^)]+\)\s*)*)\s*((?:\(result\s+[^)]+\)\s*)*)\)\)/g)]
.forEach(m => types[m[1]] = { p: m[2].match(/i32|i64|f32|f64/g) || [], r: m[3].match(/i32|i64|f32|f64/g) || [] });
const f2t = {};
[...c.matchAll(/\(func\s+(?:\$[^\s]+\s+)?\(;(\d+);\)\s+\(type\s+(\d+)\)/g)]
.forEach(m => f2t[m[1]] = m[2]);
let wit = [`package local:${base};`, '', `world ${base}-world {`];
const ims = {};
[...c.matchAll(/\(import\s+"([^"]+)"\s+"([^"]+)"\s+\(func\s+(?:\$[^\s]+\s+)?\(;(\d+);\)/g)]
.forEach(m => {
const [_, mod, field, fIdx] = m;
if (!ims[mod]) ims[mod] = [];
const t = types[f2t[fIdx]] || { p: [], r: [] };
ims[mod].push(` ${field.replace(/_/g, '-')}: ${buildS(t.p, t.r, field)};`);
});
Object.entries(ims).forEach(([m, fs]) => {
wit.push(` import ${m.replace(/_/g, '-')}: interface {`, ...fs, ` }`);
});
[...c.matchAll(/\(export\s+"([^"]+)"\s+\(func\s+(\d+)\)\)/g)]
.forEach(m => {
const [_, name, fIdx] = m;
if (name === 'memory') return;
const t = types[f2t[fIdx]] || { p: [], r: [] };
wit.push(` export ${name.replace(/_/g, '-')}: ${buildS(t.p, t.r, name)};`);
});
return wit.concat('}').join('\n');
}
const args = process.argv.slice(2);
if (args.length > 0) console.log(watToWit(args[0]));
Failing on console_log import
package local:quic;
world quic-world {
// Matches your printed imports exactly
import env: interface {
console-log: func(val: u32);
get-time-ns: func() -> s64;
random-fill: func(ptr: u32);
}
// Matches the qz_ exports from your wasm-objdump
export qz-alloc: func(size: u32) -> u32;
export qz-free: func(ptr: u32, size: u32);
export qz-set-cert: func(ptr: u32, len: u32);
export qz-set-key: func(ptr: u32, len: u32);
export qz-init-server: func() -> u32;
export qz-init-client: func() -> u32;
export qz-deinit: func();
export qz-recv-packet: func(ptr: u32, len: u32) -> s32;
export qz-send-packets: func() -> s32;
export qz-on-timeout: func();
export qz-next-timeout-ns: func() -> s64;
export qz-is-established: func() -> u32;
export qz-is-closed: func() -> u32;
export qz-poll-event: func(ptr: u32) -> s32;
export qz-stream-send: func(id: u64, ptr: u32, len: u32) -> s32;
export qz-stream-read: func(id: u64, ptr: u32, len: u32) -> s32;
export qz-stream-close: func(id: u64);
export qz-datagram-send: func(ptr: u32, len: u32) -> s32;
export qz-datagram-recv: func(ptr: u32, len: u32) -> s32;
// WebTransport specific exports
export qz-wt-accept-session: func() -> s32;
export qz-wt-read-stream: func(id: u64, ptr: u32, len: u32) -> s32;
export qz-wt-send-stream: func(id: u64, ptr: u32, len: u32) -> s32;
export qz-wt-close-stream: func(id: u64);
export qz-wt-read-datagram: func(ptr: u32, len: u32) -> s32;
export qz-wt-send-datagram: func(ptr: u32, len: u32) -> s32;
export qz-wt-close-session: func();
}
Looks like the console_log import should be console-log (i.e. dash vs. underscore)
It is a dash - in WIT. It's an underscore as an export from WASM.
Right, but wit-component is expecting the export name to have a dash rather than an underscore, hence the error.
I don't get it. That's the source code in the core WebAssembly Module. I filed an issue https://github.com/bytecodealliance/wasm-tools/issues/2483
There's no dash here https://github.com/guest271314/webtransport/blob/wasm/webtransport-example.js#L53C1-L68C33
const {instance} = await WebAssembly.instantiate(wasmBytes, {
env: {
// memory,
get_time_ns: () => BigInt(Math.round(performance.now() * 1_000_000)),
console_log: (ptr, len) => {
const bytes = new Uint8Array(instance.exports.memory.buffer,ptr,len);
console.log("[wasm]", new TextDecoder().decode(bytes));
}
,
random_fill: (ptr, len) => {
crypto.getRandomValues(new Uint8Array(instance.exports.memory.buffer,ptr,len), );
}
,
},
});
const wasm = instance.exports;
Thanks for opening the issue. I left a comment there.
Last updated: Apr 12 2026 at 23:10 UTC