WGSL — VS Code extension
Diagnostics, hover, go-to-definition, and semantic highlighting for
WGSL. Every semantic decision flows from a wasm-compiled libwgsl
frontend (the same pipeline that ships in ../include/wgsl.h).
Design — wasm first, TS minimum
Conventional WGSL extensions ship a TextMate grammar (regex-driven)
for instant offline coloring, then layer LSP semantic tokens on top
for resolver-aware refinement. This extension deliberately omits
the regex grammar — every coloured byte comes from the wasm
frontend's wgsl_semantic_tokens walk over the resolved AST.
Tradeoff: until the wasm bundle finishes loading on extension
activation (~50 ms cold), the document is uncoloured. In return:
- one source of truth for what every identifier means;
- no second classification regex that can drift away from the
real spec rules over time;
- the same engine that produces the diagnostics also produces the
highlights, so they can never disagree.
language-configuration.json is the only declarative metadata: it
tells VS Code which characters are bracket pairs and what the
comment markers look like, purely for editing UX
(auto-close-bracket, comment-toggle). No grammar.
Architecture
(wasm bundle)
wgsl_compiler.{js,wasm}
│
▼ WGSLClient (FFI wrapper, src/extension.ts)
│
▼
DocumentCompiler ── for each .wgsl document:
│ 1. find sibling `_shared.wgsl`
│ 2. split on `// --- KERNEL: <name> ---`
│ 3. build N synthetic sources:
│ shared ++ preamble ++ section_body
│ 4. wgsl_check on each → WGSLResult * per section
│ 5. cache by (uri, version)
▼
{ split, results[] }
│
┌────────┼─────────┬──────────────┐
▼ ▼ ▼ ▼
diags hover definition semantic-tokens
│ │ │ │
▼ ▼ ▼ ▼
DiagCol Hover Location SemanticTokensBuilder
│ │ │ │
└─────── line/col remap: synth → original ─────────────┘
Kernel-split convention
Multi-kernel .wgsl files (the WebGPU ML pattern: one resource set
per // --- KERNEL: <name> --- section, helper preamble at the top,
shared utilities in a sibling _shared.wgsl) are recognised
automatically. The runtime engine that ships these shaders prepends
_shared.wgsl and splits each section before handing it to the
driver; the extension does the same to keep diagnostics in sync.
Example: activation.wgsl declares two kernels (gelu_inplace,
swiglu_combine), both reference flat_id from _shared.wgsl,
both redeclare n as their own uniform. Without kernel-split, the
extension would flag flat_id undeclared and n redeclaration
(false positives — those are real WGSL semantics on the fused
single-module view, but the engine never compiles it that way).
With kernel-split, each section is its own clean compilation.
Diagnostics in _shared.wgsl are surfaced when the user opens
_shared.wgsl itself, never on the dependent files.
When _shared.wgsl is saved, every open .wgsl document is
re-compiled so its dependents catch any new errors.
Build
The wasm bundle lives in the parent project at ../.build/wasm/.
Build the bundle, then the extension:
cd .. # project root
make wasm # produces .build/wasm/wgsl_compiler.{js,wasm}
cd extension
npm install
npm run compile # copies wasm/ + tsc compile → out/extension.js
npm run watch keeps the TypeScript compiler running. Bundle
copies happen as part of the build:wasm script every npm run compile.
Run from VS Code
- Open the
extension/ directory in VS Code.
- Press F5 (or Run → Start Debugging) to launch a new Extension
Development Host.
- Open any
.wgsl file — diagnostics appear in the Problems panel,
hover shows the resolved type, Cmd-click jumps to declarations,
semantic-tokens colour the source according to the resolver's
classification.
Capabilities matrix
| Feature |
Status |
Source |
| Error diagnostics |
✅ |
wgsl_check + wgsl_diagnostic* |
| Hover (resolved type) |
✅ |
wgsl_hover_at_into |
| Go-to-definition |
✅ |
wgsl_definition_at_into |
| Semantic-tokens colour |
✅ |
wgsl_semantic_tokens |
| Bracket / comment UX |
✅ |
language-configuration.json |
| Find references |
✅ |
sweep over wgsl_semantic_tokens + wgsl_definition_at_into |
| Rename symbol |
✅ |
same sweep + WorkspaceEdit (refused on shared decls) |
| Code completion |
✅ |
static keyword/type/builtin tables + file-local decls (heuristic) |
Limitations
- Cold-start flash: source has no colour for ~50 ms while the
wasm bundle initialises on activation. Acceptable for v1.
- Position encoding: WGSL columns are 1-based byte distances;
VS Code uses UTF-16 code units. ASCII identifiers are exact;
non-ASCII XID names land 1–3 code units off. v1.x will switch to
positionEncodingKind: utf-8 once VS Code adopts it more widely.
- No incremental re-check: every edit re-runs the full pipeline.
The corpus benchmark says 0.37 ms/Kloc warm, so a 1 KLOC shader
is comfortably below the 16 ms frame budget on any modern CPU.
- References / rename are quadratic-ish per call: each query
resolves the cursor's decl and then walks every identifier-like
token in the document, calling
wgsl_definition_at_into per
token. For a 1 KLoc shader that's a few hundred wasm hops —
fine for a one-shot user action, but heavy enough that we don't
recompute on every keystroke. v1.x: a wgsl_uses_of_decl C
export that returns the use list in one round-trip.
- Completions are heuristic: the resolver doesn't expose
scope-at-position, so the suggestion list mixes (a) the static
WGSL keyword / type / builtin tables and (b) every named decl
the document defines. Function-local
lets show up outside
their function; a future scope-aware C query removes that noise.
- Renaming a
_shared.wgsl decl from a dependent file is
refused to avoid silently missing the other dependents — open
_shared.wgsl directly and rename there.