Stream: general

Topic: WIT language server + structured errors in wit-parser


view this post on Zulip Phoebe Szmucer (Mar 10 2026 at 13:16):

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.

view this post on Zulip Alex Crichton (Mar 10 2026 at 14:41):

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!

view this post on Zulip Alex Crichton (Mar 10 2026 at 14:41):

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

view this post on Zulip guest271314 (Apr 06 2026 at 03:01):

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]));

view this post on Zulip guest271314 (Apr 06 2026 at 03:02):

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();
}

view this post on Zulip Joel Dice (Apr 06 2026 at 13:28):

Looks like the console_log import should be console-log (i.e. dash vs. underscore)

view this post on Zulip guest271314 (Apr 07 2026 at 13:46):

It is a dash - in WIT. It's an underscore as an export from WASM.

view this post on Zulip Joel Dice (Apr 07 2026 at 13:52):

Right, but wit-component is expecting the export name to have a dash rather than an underscore, hence the error.

view this post on Zulip guest271314 (Apr 07 2026 at 13:58):

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

view this post on Zulip guest271314 (Apr 07 2026 at 14:02):

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;

view this post on Zulip Joel Dice (Apr 07 2026 at 14:07):

Thanks for opening the issue. I left a comment there.


Last updated: Apr 12 2026 at 23:10 UTC