WEP: LSP Architecture
Context
wado-vscode currently provides only TextMate syntax highlighting. Wado needs a Language Server Protocol (LSP) integration so editors can surface diagnostics, hover, go-to-definition, references, and semantic tokens on .wado files. The same language server must also power the browser-based playground on the official Wado site.
Personas with opposing needs
- Compiler developers (this repository): edit
wado-compiler, want the local build reflected in the editor within seconds, want native debuggers (gdb,eprintln!) and short rebuild cycles. - Application developers (marketplace): install the VS Code extension and immediately get diagnostics on
.wadofiles, including invscode.devandgithub.dev, with no separate toolchain install.
What exists today
wado-lspexposes anEnginewith typed methods (open_document,diagnostics,hover,definition,references,document_highlight,semantic_tokens). It is I/O-free and compatible withwasm32-unknown-unknown.wado-clihosts the stdio LSP adapter insrc/lsp.rs,src/lsp_adapter.rs,src/lsp_rpc.rs, and a filesystemCompilerHostinsrc/compiler_host.rs.wado lspruns the LSP server on the native process.wado-vscoderegisters syntax highlighting only; no LSP client.
The earlier wado-lsp/README.md proposed a Wasm-only, subprocess-less architecture with a handwritten C ABI bridge. That proposal is withdrawn by this WEP.
Constraints
- Web support is mandatory.
vscode.dev,github.dev, and the official Wado-site playground must all work. A native-subprocess-only architecture (rust-analyzer, gopls) cannot be the sole solution. - JSON-RPC overhead is acceptable. It is the normal LSP wire format.
- Modern Wasm performance is adequate. Wasmtime and V8 typically reach ~80% of native; Wasm speed is not a reason to avoid it.
wado-clicannot be compiled to Wasm. It depends onwasmtimeandhyper-util, which are host runtimes. Buildingwado-cliforwasm32-wasip2fails atio-lifetimesdue to unstable-feature gating and is fundamentally blocked bywasmtimeitself.
Build feasibility verification
To establish feasibility before this WEP was written, the following was verified against the current tree (2026-04-18):
cargo build -p wado-lsp --target wasm32-wasip2succeeds with zero source changes (cold: 61 seconds).- A throw-away binary combining
wado-lsp+wado-compiler+tokio(current-thread) +serde_jsoncompiles to awasm32-wasip2component, runs underwasmtime, reads.wadosource from stdin, and writes LSP-shaped JSON diagnostics to stdout. The round-trip reports the expected type-mismatch diagnostic with correct zero-based LSP ranges. - Incremental rebuild after touching
wado-compiler/src/lib.rs: 43 seconds forwasm32-wasip2release, 60 seconds for native dev. The Wasm build is actually faster because native dev paysopt-level = 2oncranelift-codegen.
Prerequisites to confirm at implementation time
- [x]
@vscode/wasm-wasi-coreor@vscode/wasm-component-modelcan host awasm32-wasip2component as an LSP server with bidirectional stdio. Outcome: onlywasm32-wasip1is supported today by the published@vscode/wasm-wasi-lsphelper. We fell back to the wasip1 target, whichwado-lspbuilds for without source changes. Revisit when the Wasm host gains component-model support. - [x]
vscode-languageclientcan be wired to a custom transport that bridges to the VS Code Wasm host.@vscode/wasm-wasi-lsp'sstartServer()returns aMessageTransports(reader/writer) that plugs directly intoLanguageClient.
Considered alternatives
- Native subprocess only (rust-analyzer, gopls, clangd style). Rejected: no web support, and platform-specific VSIXes are a distribution burden.
- Wasm with handwritten C ABI bridge (the current
wado-lsp/README.mdproposal). Rejected: loses LSP-standard features (cancellation, progress, streaming) offered byvscode-languageclient; duplicates JSON marshalling; complicates the compiler-developer inner loop. - Wasm-only as virtual subprocess, no native path. Rejected: compiler-developer inner loop becomes 40–60 seconds per iteration and loses
eprintln!/gdbergonomics. - Keep stdio server in
wado-cli, ship a separatewado-lsp-servercrate for the Wasm build. Rejected: two implementations of the same server, inviting drift.
Decision
Single LSP server implementation, two build targets
wado-lsp owns the entire LSP server: the Engine, the stdio JSON-RPC transport, the request dispatcher, the LSP wire types, and a FilesystemCompilerHost. A new [[bin]] wado-lsp is added. The binary builds for both native and wasm32-wasip2:
- The native build powers the desktop CLI (
wado lspdelegates to it) and the compiler-developer override (wado.serverPath). - The
wasm32-wasip2build is embedded in the VS Code extension VSIX and reused by the browser playground.
Both targets speak the same LSP JSON-RPC wire protocol over stdio.
Crate layout
wado-lsp/
├── src/
│ ├── lib.rs # Engine (existing)
│ ├── diagnostics.rs, ... # (existing)
│ ├── host.rs # FilesystemCompilerHost (moved from wado-cli)
│ └── server/
│ ├── mod.rs # pub async fn run_stdio()
│ ├── transport.rs # Content-Length header + JSON-RPC framing
│ ├── dispatch.rs # LSP method -> Engine routing
│ └── rpc.rs # LSP wire types
└── src/bin/
└── wado-lsp.rs # #[tokio::main(flavor = "current_thread")] run_stdio()
The "protocol-agnostic" framing in the current wado-lsp/README.md is removed. wado-lsp is the LSP package; its library surface exposes a typed Engine that other consumers may reuse, but its primary deliverable is the LSP server binary.
No Cargo features gate the server. All dependencies (tokio, serde_json) are unconditional. wado-compiler dominates build time, so splitting the server behind a feature flag would not meaningfully change the Engine-only consumption profile.
wado-cli is a thin client
wado-cli/src/lsp.rsreduces to callingwado_lsp::server::run_stdio().await.wado-cli/src/lsp_adapter.rsandwado-cli/src/lsp_rpc.rsare deleted; their content moves towado-lsp/src/server/.wado-cli/src/compiler_host.rseither re-exportswado_lsp::FilesystemCompilerHostor keeps only CLI-specific decorations (log-level filtering, phase timing) on top of it.
wado-cli mcp and wado-cli query are out of scope
The MCP server and the one-shot diagnostics CLI stay inside wado-cli. Both are native-only tools; neither currently has a compelling Wasm use case. A future WEP may revisit their placement. If that happens, it should follow the same pattern as this WEP: move the server implementation into a dedicated crate that builds for both native and wasm32-wasip2, and keep wado-cli as a thin dispatcher.
VS Code extension
wado-vscode gains an LSP client built on the official vscode-languageclient package. The client's ServerOptions callback has two branches:
- If
wado.serverPathis set in settings, spawn that executable as a native subprocess with stdio piped toLanguageClient. - Otherwise, launch the bundled
wado-lsp.wasmvia@vscode/wasm-wasi-core(or@vscode/wasm-component-model, depending on prerequisite validation). The Wasm host exposes the process as stdio streams;LanguageClientdrives them identically to the native case.
Settings contributed by the extension:
wado.serverPath(string, default unset) — absolute path to a nativewado-lspbinary. Compiler developers set this to${workspaceFolder}/target/debug/wado-lsp.wado.trace.server(standard LSP trace setting) — trace verbosity.
Commands:
wado.restartLanguageServer— callsclient.restart(). Bound to a default keybinding.
A vscode.workspace.createFileSystemWatcher on the bundled wado-lsp.wasm path triggers client.restart() automatically when the file changes, so a rebuild on disk reloads the running LSP with no user action.
Compiler-developer inner loop
Two complementary paths share the same server implementation:
- Native override (primary). Set
wado.serverPathto a local build.cargo watch -x 'build --bin wado-lsp'rebuilds in a few seconds on edit;wado.restartLanguageServeror an auto-restart hook reloads the LSP.eprintln!appears in the "Wado Language Server" output channel;gdb/lldbcan attach to the subprocess. - Wasm hot-reload (secondary).
mise run watch-wado-lsp-wasmrebuilds and copieswado_lsp.wasmintowado-vscode/out/on change. TheFileSystemWatcherdescribed above triggersclient.restart()automatically. No keystrokes. Incremental rebuild takes 40–60 seconds.
Both workflows use the exact same server implementation, so observed behavior does not diverge between paths.
Playground compatibility
This WEP does not design the browser playground integration. It requires only that the produced wado-lsp.wasm remain compatible with a generic browser-worker environment: no VS Code-specific imports leak into the Wasm module. A follow-up WEP will design the JS/TS host and transport (likely MessagePort-based JSON-RPC) for the public site.
Distribution
- VSIX is a single cross-platform artifact containing
wado-lsp.wasm. No per-platform VSIX is needed. - The extension is published to both the Microsoft Marketplace and Open VSX as
wado-lang.wado.
Consequences
Benefits
- One LSP implementation, not two. The same Rust code serves desktop (native), web (Wasm-in-VS-Code), and playground (Wasm-in-browser). No FFI duplication and no wire-protocol divergence.
- Zero-install for end users. A single Wasm component works on Windows, macOS, Linux,
vscode.dev, andgithub.devwithout platform-specific VSIXes. - Fast inner loop for compiler developers. Native override gives sub-second rebuilds and full debugger access; Wasm hot-reload gives 40–60-second cycles with zero manual intervention.
- Standard tooling.
vscode-languageclienthandles cancellation, progress, streaming, and future LSP features; no handwritten JSON-RPC on the TypeScript side.
Trade-offs
wado-lspis no longer a pure library. It ships a binary and depends unconditionally ontokioandserde_json. Consumers of theEngineAPI pick up these dependencies even if they only want the library.- The VS Code extension requires a Rust build step (
cargo build -p wado-lsp --target wasm32-wasip2 --release) before packaging. Amisetask orchestrates this; CI runs it on every PR. - Compiler-developer native override requires a
target/debug/wado-lspbuild on disk. The mise-managed workflow already builds all binaries, but documentation must make this explicit. wasm32-wasip2is added torust-toolchain.toml. The firstrustupsync after update pulls an extra standard library.
Breaking changes
wado-lsp/README.mdis rewritten. The "Wasm-only, no subprocess" line is withdrawn.wado-cli/src/lsp_adapter.rsandwado-cli/src/lsp_rpc.rsare removed. No external consumer is known.
Rollout
- [x] Validate
@vscode/wasm-wasi-coreor@vscode/wasm-component-modelcan hostwasm32-wasip2components as stdio LSP servers. Pivoted towasm32-wasip1(see Prerequisites). - [x] Add
wasm32-wasip1andwasm32-wasip2torust-toolchain.tomltargets. - [x] Move the stdio LSP server from
wado-clitowado-lsp. Add[[bin]] wado-lsp. Deletewado-cli/src/lsp_adapter.rsandwado-cli/src/lsp_rpc.rs. Reducewado-cli/src/lsp.rsto a delegation call. - [x] Move
FilesystemCompilerHosttowado-lsp. Wirewado-clito re-export or wrap it. - [x] Add
misetasksbuild-wado-lsp-wasmandwatch-wado-lsp-wasm(targetwasm32-wasip1). - [x] Add a CI job that builds the Wasm artifact and runs the
wado-vscodetest suite. - [x] Implement the VS Code LSP client in
wado-vscodewith both subprocess and Wasm paths, thewado.serverPathsetting, thewado.restartLanguageServercommand, and theFileSystemWatcherauto-restart. - [x] Add an end-to-end test that drives the Wasm build through an
initialize/didOpen/publishDiagnosticsexchange. - [x] Rewrite
wado-lsp/README.mdto reflect the new architecture.
Implementation notes
- The stdio server is driven by
futures::executor::block_onrather than a tokio runtime. tokio'sio-stdis unavailable onwasm32-*, andfuturesis already a transitive workspace dependency — keeping tokio out ofwado-lspshrinks the Wasm build and avoids duplicating tokio feature sets againstwado-cli(which keeps tokio for the production HTTP server). wado-lsp/src/server.rsuses synchronousstd::io::{stdin, stdout}for framing;Enginequeries remainasync fnand are awaited between reads.- Source layout uses
wado-lsp/src/server.rs+wado-lsp/src/server/{transport,dispatch,rpc}.rs(rather thanserver/mod.rs) to comply with the workspaceclippy::mod-module-fileslint.
See Also
wep-2026-01-16-source-provider-abstraction.md—CompilerHostabstraction that makes the compiler I/O-free and Wasm-buildable.wep-2026-01-11-wasi-p3-only.md— Wado's position on WASI versions; the LSP server targets p2 for VS Code hosting reasons, which is independent of the runtime targets used by compiled Wado programs.- Language Server Protocol specification — wire protocol for the transport implemented in
wado-lsp/src/server/.
