wgsl.run — VS Code extension
Diagnostics, hover, go-to-definition, find references, rename,
completions, and semantic highlighting for WGSL. Every semantic
decision flows from a wasm-compiled libwgsl frontend — the same
spec-conformant pipeline that ships as a C library.
v1.0 status
|
|
| Spec coverage |
W3C WGSL Recommendation §3–§18 — full |
| WGSL spec pin |
CRD-2026-05-07 |
| Engine |
libwgsl wasm (single source of truth) |
| Performance |
~0.37 ms / Kloc warm — well under the 16 ms frame budget |
| Bundle |
wasm + ~1 KLoc TypeScript glue, no regex grammar |
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;
- the engine that powers diagnostics is the same one shipped in
libwgsl.a for C consumers — what you see in the editor is what
any downstream pipeline will see.
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. resolve preamble (wgslconfig.toml or _shared.wgsl)
│ 2. split on `// --- KERNEL: <name> ---`
│ 3. build N synthetic sources:
│ preamble ++ file_preamble ++ section_body
│ 4. wgsl_check on each → WGSLResult * per section
│ 5. cache by (uri, version)
▼
{ split, results[] }
│
┌────┬────┬────┬────┬────┬─────┬──────────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼
diags hover def refs rename completion semantic-tokens
│ │ │ │ │ │ │
└─── line/col remap: synth → original ┘
Kernel-split + preamble injection
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 file) are recognised automatically. The
runtime engine that ships these shaders prepends the shared preamble
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 a shared preamble,
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.
Preamble resolution
Two paths, in priority order:
wgslconfig.toml (recommended) — discovered by walking up
from the document's directory. Driven by the C-side
wgsl_project_match (same logic as the CLI). Supports a list of
preamble files + glob-based auto_inject_into rules. Files
listed never inject into themselves.
# wgslconfig.toml
[preamble]
files = [
"_enables.wgsl", # enable subgroups; enable f16;
"_shared.wgsl", # helpers used by every kernel
]
auto_inject_into = ["*.wgsl"]
Legacy _shared.wgsl — back-compat for projects without a
config. If a sibling _shared.wgsl exists, it's prepended.
Diagnostics in preamble files are surfaced when the user opens the
preamble file itself, never on the dependent files. When a preamble
is saved, every open .wgsl document is re-compiled so its dependents
catch any new errors.
Try it
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 |
| 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) |
| Semantic-tokens colour |
✅ |
wgsl_semantic_tokens |
| Bracket / comment UX |
✅ |
language-configuration.json |
wgslconfig.toml |
✅ |
wgsl_project_open_from_string + wgsl_project_match |
| Kernel-split |
✅ |
// --- KERNEL: <name> --- markers |
| Re-check on preamble save |
✅ |
onDidSaveTextDocument workspace walk |
Configuration
The extension reads wgslconfig.toml from the closest ancestor
directory of the open document. Without a config, the legacy
_shared.wgsl sibling lookup applies.
No VS Code settings are exposed — every behavior is driven by source
files (config + preambles + the wasm engine's diagnostics).
| Operation |
Cold |
Warm |
| Activation (wasm boot) |
~50 ms |
(in-process) |
wgsl_check per Kloc |
~0.4 ms |
~0.37 ms |
| Hover / definition (cached) |
~0.1 ms |
~0.05 ms |
| Find references (1 Kloc) |
~2–5 ms |
(per query) |
Per-keystroke re-check on a 1 Kloc shader sits comfortably under the
16 ms frame budget on any modern CPU.
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 preamble decl from a dependent file is refused to
avoid silently missing the other dependents — open the preamble
file directly and rename there.
What's powered by the same engine
The extension and the C library are the same compile pipeline.
Anything flagged in the IDE is exactly what wgsl_check(source) would
report from C / WASM. The implementation covers WGSL §3–§18 in full:
146 reserved words, all 17 attributes, all 19 type constructors, all
11 atomic builtins, all 16 pack/unpack builtins, all 15 texture
builtins, behavior-set analysis, layout calculator, uniformity
analysis, alias analysis — the whole grammar and rule set.
v1.x roadmap
- UTF-8 position encoding (
positionEncodingKind: utf-8)
- Scope-aware completion via a new C-side scope query
- One-round-trip
wgsl_uses_of_decl for references / rename
- WASM bundle size reduction (target < 200 KB gzipped, < 50 ms init)
- LSP enrichments: signature help, document symbols, inlay hints,
code actions ("add
enable f16;", "import preamble", …)
License
MIT.