Documentation
¶
Overview ¶
Package wgslender minifies, validates, lints, reflects over and compiles WGSL (WebGPU Shading Language) shaders.
It is a pure-Go binding. The wgslender engine is written in Zig and ships here as a WebAssembly module embedded in the package and executed by wazero, so building it needs no cgo, no C toolchain, and no step beyond `go build` on any platform Go targets.
Initialisation ¶
There is none to do. The embedded module is compiled on first use and reused for the lifetime of the process: the first call pays roughly 130 ms, later calls do not. The npm package instead makes the caller await an explicit initialize(); Go's lazy initialisation makes that ceremony unnecessary.
Contexts and concurrency ¶
Every function that reaches the engine takes a context.Context first, and all of them are safe to call from any number of goroutines.
Behind them is a small pool of WebAssembly instances — one per processor, up to eight, built on demand. One instance is one single-threaded allocator, so each call has an instance to itself while it runs and the pool is what lets independent calls still run at the same time. A program that never calls from two goroutines at once never builds more than one instance; each holds a couple of megabytes, and one that grows unusually large after an unusually large shader is discarded rather than kept.
Cancelling a context stops a call from starting: it will not queue for an instance, and it returns an error wrapping context.Canceled or context.DeadlineExceeded. It does not interrupt a call already running. wazero can be asked to check for cancellation inside the guest, but that costs four and a half times the run time of every call, which is a poor trade against work measured in tens of microseconds.
Text ¶
WGSL is defined over UTF-8, and a Go string is only bytes, so every string this package is given — shader source, identifiers, type text — must be valid UTF-8. One that is not is refused with ErrInvalidUTF8 rather than answered, because the engine would copy the stray bytes into a reply that no JSON decoder can read back faithfully.
Errors ¶
A shader's own problems are data, not errors. Source that does not parse, does not type-check or trips a lint rule is reported in the result — as MinifyResult.Errors, Validation.Diagnostics, LintReport.Diagnostics or Reflection.Errors — and the call itself succeeds. A returned error means the call could not be made or could not be trusted.
The exceptions are the calls that have nothing to report when they fail. Compile is one: every other function still has an answer for a shader it could not read, while a compiler with nothing to compile has no module to hand back, so source that does not parse is a *CompileError there. The refactor operations are the rest — a rename that cannot be performed has no edits, and an empty edit list would say the opposite, that there was nothing to do. Their reasons are sentinels: ErrSymbolNotFound, ErrInvalidIdentifier, ErrNotRemovable and their kin.
Refactoring ¶
Twelve operations work on a shader's symbols rather than on its text as a whole: finding where a symbol is mentioned, renaming it, retyping it, removing it.
Address the symbol either by a byte offset into the source or by a StableID. An offset is what a cursor gives you, and it stops being right the moment anything before it changes; a StableID names the symbol itself and survives edits elsewhere in the file. StableIDAtOffset turns one into the other, and Reflect hands out IDs for everything it describes.
The ones that change something come in pairs. The plain form — Rename, ChangeType, RemoveDeclaration — returns Edit values and changes nothing, which is what an editor wants, having its own buffer to splice into and its own undo stack to record. The Apply form does the splicing and hands back the rewritten source, which is what a script wants.
None of them type-check what they produce. A rename can collide, a removed function leaves its callers behind, and a replacement type is spliced in verbatim whether or not it is a type. Run Validate on the result when that matters.
Index ¶
- Variables
- func BindGroups(bindings []Binding) map[uint32]map[uint32]Binding
- func ReflectJSON(ctx context.Context, source string) ([]byte, error)
- func Version(ctx context.Context) (string, error)
- type AccessMode
- type AddressSpace
- type Alias
- type Applied
- type ArrayInfo
- type Binding
- type CompileError
- type CompiledShader
- type Declarations
- type Diagnostic
- type Edit
- func ChangeType(ctx context.Context, source string, id StableID, newType string) ([]Edit, error)
- func RemoveDeclaration(ctx context.Context, source string, id StableID) ([]Edit, error)
- func Rename(ctx context.Context, source string, offset int, newName string) ([]Edit, error)
- func RenameByID(ctx context.Context, source string, id StableID, newName string) ([]Edit, error)
- type EntryPoint
- type Field
- type Fix
- type Function
- type IOVar
- type Interpolation
- type LintConfig
- type LintFixOutcome
- type LintReport
- type MinifiedShader
- type MinifyOptions
- type MinifyResult
- type Opt
- type Override
- type Pack
- type Position
- type Range
- type Reference
- type Reflection
- type RelatedInfo
- type RuleSetting
- type Severity
- type ShaderStage
- type Span
- type StableID
- type Strictness
- type StructLayout
- type TextureDimension
- type TextureKind
- type TypeInfo
- type TypeKind
- type Validation
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInternal reports that the engine failed for a reason it cannot // describe: it ran out of memory, or a length overflowed its 32-bit // address space. ErrInternal = wasmabi.ErrInternal // ErrSourceTooLarge reports an input larger than the engine's 32-bit // address space. ErrSourceTooLarge = wasmabi.ErrSourceTooLarge // ErrInvalidUTF8 reports an argument that is not valid UTF-8. // // WGSL is defined over UTF-8 text, and Go's string type does not enforce // that, so this package checks. It has to: the engine copies unknown bytes // through into its JSON replies verbatim, and every JSON decoder then // silently substitutes U+FFFD for them — the answer would come back // corrupted rather than refused. (Rust gets this from &str for free and // the npm package does the substituting.) ErrInvalidUTF8 = errors.New("wgslender: not valid UTF-8") // ErrInvalidOffset reports a byte offset the ABI cannot carry: a negative // one, or one beyond the engine's 32-bit address space. // // An offset merely past the end of the source is not one of these. The // engine answers that nothing is there, which is true and useful, so it is // passed through rather than refused. ErrInvalidOffset = errors.New("wgslender: offset out of range") )
The errors this package reports for a call that could not be made or could not be believed. A shader's own problems are never one of these: they arrive as data — MinifyResult.Errors, Validation.Diagnostics and their kin.
var ( // ErrParse reports source the parser abandoned. // // It is narrower than "invalid WGSL". The parser recovers from most // mistakes and hands back a module full of errors, and a refactor over // that module answers normally. This is the residue: the shapes it cannot // rebuild anything from. ErrParse = errors.New("wgslender: the shader could not be parsed") // ErrSymbolNotFound reports an offset that names no symbol, or a // [StableID] this source does not contain. // // [FindReferences] is the exception that proves the rule: it answers the // same situation with no references at all, because asking it about // wherever a cursor happens to be is the normal way to use it. ErrSymbolNotFound = errors.New("wgslender: no such symbol") // ErrInvalidIdentifier reports a new name WGSL will not accept — a // keyword, a reserved word, an empty string, or anything that is not // spelled like an identifier. ErrInvalidIdentifier = errors.New("wgslender: not a valid WGSL identifier") // ErrNoTypeAnnotation reports a symbol with no declared type to replace, // such as an inferred let or an entry point that returns nothing. // [LocateType] answers the same question without changing anything. ErrNoTypeAnnotation = errors.New("wgslender: no type annotation to replace") // ErrNotRemovable reports something that is not a declaration in its own // right — a struct member, a function parameter — and so cannot be deleted // without rewriting what encloses it. ErrNotRemovable = errors.New("wgslender: not a removable declaration") // ErrStableIDTooLong reports a symbol whose ID would exceed the engine's // buffer, which takes a name or a nesting depth in the thousands. Looking // up an ID that long is not this error — it simply is not found. ErrStableIDTooLong = errors.New("wgslender: the stable ID is too long") )
The refactor operations' failures, one for each reason the engine can name. A shader's own problems are still data everywhere else in this package; these are here because a refactor that cannot be performed has no edits to report, and an empty edit list would say the opposite — that there was nothing to do.
Functions ¶
func BindGroups ¶
BindGroups arranges bindings the way a WebGPU host consumes them: by group, then by slot within the group.
It is a map of maps rather than slices because bind groups are sparse. A shader may bind @group(0) @binding(0) and @group(0) @binding(2) and nothing between them, and may skip a whole group; indexing a slice would invent the gaps as zero values. The returned map is never nil, so an empty grid is still safe to index.
for group, slots := range wgslender.BindGroups(r.Bindings) {
for slot, b := range slots {
fmt.Println(group, slot, b.Name, b.AddressSpace)
}
}
Example ¶
ExampleBindGroups arranges the bindings the way a WebGPU host consumes them, by group and then by slot.
package main
import (
"context"
"fmt"
"log"
"maps"
"slices"
"github.com/HugoDaniel/wgslender/packages/go/wgslender"
)
// exampleShader is what the examples work on: two resources, a helper the
// minifier is free to rename, and one compute entry point.
const exampleShader = `struct Params {
resolution: vec2f,
time: f32,
}
@group(0) @binding(0) var<uniform> params: Params;
@group(1) @binding(2) var<storage, read_write> data: array<vec4f>;
fn luminance(color: vec3f) -> f32 {
return dot(color, vec3f(0.2126, 0.7152, 0.0722));
}
@compute @workgroup_size(8, 8, 1)
fn main(@builtin(global_invocation_id) id: vec3u) {
let uv = vec2f(id.xy) / params.resolution;
let lum = luminance(vec3f(uv, params.time));
data[id.x] = vec4f(lum, lum, lum, 1.0);
}
`
func main() {
r, err := wgslender.Reflect(context.Background(), exampleShader)
if err != nil {
log.Fatal(err)
}
groups := wgslender.BindGroups(r.Bindings)
// The map is a map because bind groups are sparse — a shader may use
// @group(0) and @group(1) and nothing between — so ranging over it needs
// the keys sorted to print in a fixed order.
for _, group := range slices.Sorted(maps.Keys(groups)) {
for _, slot := range slices.Sorted(maps.Keys(groups[group])) {
b := groups[group][slot]
fmt.Printf("group %d slot %d: %s (%s)\n", group, slot, b.Name, b.AddressSpace)
}
}
}
Output: group 0 slot 0: params (uniform) group 1 slot 2: data (storage)
func ReflectJSON ¶
ReflectJSON is Reflect without the decoding: the engine's own schema-v2 document, verbatim.
Reach for it when you want to forward the reflection somewhere else — a build manifest, a generator, another language — or when you need a key that Reflection does not model. The bytes are a fresh copy and yours to keep.
Types ¶
type AccessMode ¶
type AccessMode string
An AccessMode is how a variable may be used (WGSL §7.3). It is spelled only where WGSL spells it — a storage binding and a pointer — and is empty elsewhere.
const ( // AccessRead lets the shader only read. AccessRead AccessMode = "read" // AccessWrite lets the shader only write. AccessWrite AccessMode = "write" // AccessReadWrite lets the shader do both. AccessReadWrite AccessMode = "read_write" )
type AddressSpace ¶
type AddressSpace string
An AddressSpace is where a variable lives (WGSL §7.3). Bindings are in uniform, storage, or — for textures and samplers — handle.
const ( // AddressSpaceFunction holds a variable local to one function call. AddressSpaceFunction AddressSpace = "function" // AddressSpacePrivate holds module-scope state private to one invocation. AddressSpacePrivate AddressSpace = "private" // AddressSpaceWorkgroup holds state shared by a compute workgroup. AddressSpaceWorkgroup AddressSpace = "workgroup" // AddressSpaceUniform holds a read-only binding uploaded by the host. AddressSpaceUniform AddressSpace = "uniform" // AddressSpaceStorage holds a buffer binding, writable when its // [AccessMode] says so. AddressSpaceStorage AddressSpace = "storage" // AddressSpaceHandle holds textures and samplers. WGSL leaves it // unspellable in source; the engine infers it from the type. AddressSpaceHandle AddressSpace = "handle" )
type Alias ¶
type Alias struct {
// Name is the alias as written and NameMapped what it became.
Name string `json:"name"`
NameMapped string `json:"nameMapped"`
// NameOffset is the byte offset of the name in the original source.
NameOffset int `json:"nameOffset"`
// StableID names this alias across reparses.
StableID StableID `json:"stableId,omitempty"`
// DeclSpan covers the whole declaration.
DeclSpan Span `json:"declSpan,omitzero"`
// Type is the aliased type as written and TypeMapped what it became.
Type string `json:"type"`
TypeMapped string `json:"typeMapped"`
// TypeInfo is the structured type it resolves to.
TypeInfo *TypeInfo `json:"typeInfo,omitempty"`
}
An Alias is a type alias declaration.
type Applied ¶
type Applied struct {
// Source is the rewritten shader.
Source string
// Edits is what was done to produce it, at offsets into the *original*
// source. They are of no use for splicing — that already happened — and of
// every use for showing a diff or moving a cursor.
Edits []Edit
}
An Applied is the result of an operation that performed its own edits.
func ChangeTypeApply ¶
func ChangeTypeApply(ctx context.Context, source string, id StableID, newType string) (Applied, error)
ChangeTypeApply replaces a symbol's type annotation and returns the rewritten source. The replacement is unchecked, exactly as ChangeType describes.
func RemoveDeclarationApply ¶
RemoveDeclarationApply deletes a declaration and returns the rewritten source. It leaves the uses of what it deleted behind, exactly as RemoveDeclaration describes.
func RenameApply ¶
RenameApply renames the symbol at a byte offset and returns the rewritten source.
What comes back parses, but nothing promises it is correct: renaming a symbol to a name already taken in the same scope produces a shader that Validate will reject. The rewrite is a splice, not a refactoring engine.
Example ¶
ExampleRenameApply renames a symbol and hands back the rewritten shader. The edits alongside it are at offsets into the *original* source — they are for showing a diff, not for splicing, because the splicing already happened.
package main
import (
"context"
"fmt"
"log"
"strings"
"github.com/HugoDaniel/wgslender/packages/go/wgslender"
)
func main() {
const shader = `fn scale(v: f32) -> f32 { return v * 2.0; }
@fragment
fn main() -> @location(0) vec4f {
let s = scale(0.5);
return vec4f(s);
}
`
applied, err := wgslender.RenameApply(context.Background(),
shader, strings.Index(shader, "scale"), "double")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%d edits\n", len(applied.Edits))
fmt.Print(applied.Source)
}
Output: 2 edits fn double(v: f32) -> f32 { return v * 2.0; } @fragment fn main() -> @location(0) vec4f { let s = double(0.5); return vec4f(s); }
type ArrayInfo ¶
type ArrayInfo struct {
// Depth is which dimension this is, starting at 1 for the outermost.
Depth int `json:"depth"`
// ElementCount is how many elements this dimension holds, nil for a
// runtime-sized array — one whose length the host decides when it binds a
// buffer.
ElementCount *int `json:"elementCount"`
// ElementStride is the byte distance between elements. It is known even
// when the count is not.
ElementStride int `json:"elementStride"`
// TotalSize is the dimension's byte size, nil for a runtime-sized array.
TotalSize *int `json:"totalSize"`
// ElementType is the element type as written — an alias is not resolved
// away here — and ElementTypeMapped what it became after minification.
ElementType string `json:"elementType"`
ElementTypeMapped string `json:"elementTypeMapped"`
// ElementLayout is the element's layout, present when the elements are
// structs.
ElementLayout *StructLayout `json:"elementLayout,omitempty"`
// Array is the next dimension in, present for an array of arrays.
Array *ArrayInfo `json:"array,omitempty"`
}
An ArrayInfo describes one dimension of an array-typed binding, counting outward-in.
type Binding ¶
type Binding struct {
// Group is the @group index and Binding the @binding index.
Group uint32 `json:"group"`
Binding uint32 `json:"binding"`
// Name is the identifier as written in the source.
Name string `json:"name"`
// NameMapped is what that identifier became after minification. It equals
// Name unless the reflection came from [MinifyAndReflect], and even then
// only handle-space bindings are renamed by default — the host binds
// against uniform and storage names, so those are left alone.
NameMapped string `json:"nameMapped"`
// NameOffset is the byte offset of the name in the original source.
NameOffset int `json:"nameOffset"`
// StableID names this variable across reparses.
StableID StableID `json:"stableId,omitempty"`
// DeclSpan covers the whole declaration, attributes through semicolon;
// TypeSpan covers just the type.
DeclSpan Span `json:"declSpan,omitzero"`
TypeSpan Span `json:"typeSpan,omitzero"`
// AddressSpace is where the resource lives.
AddressSpace AddressSpace `json:"addressSpace"`
// AccessMode is empty unless the declaration spells one, which in
// practice means storage bindings.
AccessMode AccessMode `json:"accessMode,omitempty"`
// Type is the type as written and TypeMapped what it became after
// minification.
Type string `json:"type"`
TypeMapped string `json:"typeMapped"`
// Layout is the memory layout, present when the type is a struct. The
// same layout is in [Reflection.Structs] under the type's name.
Layout *StructLayout `json:"layout,omitempty"`
// Array describes the array, present when the type is one. It is a
// different view from TypeInfo's — this one counts dimensions and
// strides, that one describes the element type.
Array *ArrayInfo `json:"array,omitempty"`
// TypeInfo is the structured type, nil when the engine could not resolve
// it.
TypeInfo *TypeInfo `json:"typeInfo,omitempty"`
// Relations names other bindings used together with this one — the
// samplers a texture is sampled with, and back again.
Relations []string `json:"relations,omitempty"`
}
A Binding is one @group/@binding variable — a resource the host has to supply.
type CompileError ¶
type CompileError struct {
// Diagnostics is every parse error, in source order, not just the first.
Diagnostics []Diagnostic
}
A CompileError reports a shader Compile could not read.
It is the one place in this package where a shader's own problem is a Go error rather than data. Everywhere else there is still an answer to give — minification hands back the original source, validation hands back diagnostics — but a compiler with nothing to compile has no module to return, and an empty module is not an answer.
func (*CompileError) Error ¶
func (e *CompileError) Error() string
Error reports the first parse error, and how many followed it. The rest are in CompileError.Diagnostics, with their positions.
type CompiledShader ¶
type CompiledShader struct {
// WASM is the module. It imports nothing, so instantiating it needs no
// import object and no host functions, and it exports exactly two things:
// generate() and the memory it writes into.
WASM []byte
// OriginalSize is the byte length of the WGSL that was handed in — not of
// the module, and not of the text the module expands to, which is smaller
// because it is minified. Compare it against len(WASM) for what compiling
// bought.
OriginalSize int
}
A CompiledShader is a WebAssembly module that hands back one shader.
It is a compressed shader, not a compiled pipeline: the module carries the minified WGSL byte-pair encoded, behind a decoder of about a hundred bytes, and the text it produces still goes to createShaderModule on the other side.
func Compile ¶
func Compile(ctx context.Context, source string, opts *MinifyOptions) (CompiledShader, error)
Compile turns a shader into a WebAssembly module that regenerates it at run time. A nil opts means wgslender's defaults; see MinifyOptions.
Run what comes back with any WebAssembly host. In Go that is the same wazero this package already depends on:
compiled, err := wgslender.Compile(ctx, source, nil)
// …
runtime := wazero.NewRuntime(ctx)
defer runtime.Close(ctx)
mod, err := runtime.Instantiate(ctx, compiled.WASM)
// …
out, err := mod.ExportedFunction("generate").Call(ctx)
// …
wgsl, _ := mod.Memory().Read(0, uint32(out[0]))
Which options apply ¶
Four of them: MinifyOptions.MinifyIdentifiers, MinifyOptions.MangleExternalBindings, MinifyOptions.KeepNames and MinifyOptions.TreeShaking. Those decide what the shader says.
The rest decide how it is written down, and the compiler overrides them: it always sorts declarations and reuses short names across sibling scopes, because both compress better, and it leaves the syntax rewrites off. So setting MinifyWhitespace, MinifySyntax, SortDeclarations, ScopeLocalRename, PreserveUniformStructTypes or either source-map option changes nothing here. They are ignored rather than refused, since they are the same options type Minify takes.
One consequence worth expecting: the text the module expands to is not the text Minify would have produced from the same shader with the same options.
Errors ¶
A shader that does not parse is a *CompileError carrying the parse errors, because there is no module to hand back:
var cerr *wgslender.CompileError
if errors.As(err, &cerr) {
for _, d := range cerr.Diagnostics { … }
}
A shader that parses always compiles, however little sense it makes: the compiler never type-checks, so an undeclared name or a type mismatch reaches the module untouched. Call Validate if you need to know.
The other errors are the usual ones for a call that could not be made or trusted — ErrInvalidUTF8, ErrSourceTooLarge, ErrInternal.
Example ¶
ExampleCompile turns a shader into a binary module, and shows the one place in this package where a shader's own problem is a Go error: a compiler with nothing to compile has no module to hand back.
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/HugoDaniel/wgslender/packages/go/wgslender"
)
// exampleShader is what the examples work on: two resources, a helper the
// minifier is free to rename, and one compute entry point.
const exampleShader = `struct Params {
resolution: vec2f,
time: f32,
}
@group(0) @binding(0) var<uniform> params: Params;
@group(1) @binding(2) var<storage, read_write> data: array<vec4f>;
fn luminance(color: vec3f) -> f32 {
return dot(color, vec3f(0.2126, 0.7152, 0.0722));
}
@compute @workgroup_size(8, 8, 1)
fn main(@builtin(global_invocation_id) id: vec3u) {
let uv = vec2f(id.xy) / params.resolution;
let lum = luminance(vec3f(uv, params.time));
data[id.x] = vec4f(lum, lum, lum, 1.0);
}
`
func main() {
shader, err := wgslender.Compile(context.Background(), exampleShader, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("input:", shader.OriginalSize, "bytes; module:", len(shader.WASM), "bytes")
_, err = wgslender.Compile(context.Background(), "fn broken( {}", nil)
var cerr *wgslender.CompileError
if errors.As(err, &cerr) {
for _, d := range cerr.Diagnostics {
fmt.Printf("%d:%d %s\n", d.Line, d.Column, d.Message)
}
}
}
Output: input: 497 bytes; module: 546 bytes 1:12 expected ')'
type Declarations ¶
type Declarations int
Declarations says whether FindReferences counts a symbol's declaration among the references to it.
It is a named choice rather than a bool argument, so that the call site says which it means. The zero value includes the declaration, which is the fuller answer.
const ( // WithDeclaration counts the declaration. It sorts first, and it is the // one reference whose IsWrite is true in a shader that never assigns to // the symbol again. WithDeclaration Declarations = iota // WithoutDeclaration leaves it out, which is what "find usages" means in // most editors. WithoutDeclaration )
type Diagnostic ¶
type Diagnostic struct {
// Severity is how serious it is.
Severity Severity
// Message is human-readable text, with no position prefix.
Message string
// Code is the stable identifier, such as E0100 or W0001. Parse errors
// often have none, so this is empty more than rarely.
Code string
// Line is the 1-based line of the first offending byte.
Line int
// Column is the 1-based column of the first offending byte.
Column int
// SpecRef points at the section of the WGSL specification this rests on,
// when there is one.
SpecRef string
// Source names what produced the diagnostic. Lint rules say
// "wgslender-lint"; the parser and validator leave it empty, which is how
// [LintReport.Diagnostics] can be told apart despite arriving in one array.
Source string
// Related is context elsewhere in the file — the earlier declaration a
// name shadows, say.
Related []RelatedInfo
// Fix is the rewrite that resolves this diagnostic, present only on the
// ones [LintFix] can apply. It describes a splice: replace Range with Text.
Fix *Fix
}
A Diagnostic is one thing the engine has to say about a shader.
Positions are 1-based, the way every editor and compiler prints them.
type Edit ¶
type Edit struct {
// Span is the half-open byte range to replace.
Span
// NewText is what to put there. Empty means a deletion.
NewText string `json:"newText"`
}
An Edit is a splice: replace the source in Span with NewText.
Edits within one result never overlap, so they can be applied in any order — though applying them from the end backwards is the only order that does not invalidate the offsets of the ones still to come. The apply-suffixed operations do this for you.
func ChangeType ¶
ChangeType produces the single edit that replaces a symbol's type annotation.
The replacement is spliced in verbatim and is never checked: "not a type" is accepted and produces a shader that does not parse. Writing something WGSL will have is the caller's job.
A symbol with no annotation to replace — an inferred let, an entry point with no return type — is ErrNoTypeAnnotation. LocateType answers the same question without changing anything.
func RemoveDeclaration ¶
RemoveDeclaration produces the single edit that deletes a declaration.
It deletes the declaration and only the declaration. Calls to a removed function, and uses of a removed variable, are left exactly where they were, so the result usually no longer type-checks — check with Validate if that matters. Removing what is not a declaration in its own right, such as a struct member or a function parameter, is ErrNotRemovable.
func Rename ¶
Rename produces the edits that rename the symbol at a byte offset, without applying them. Use RenameApply to get the rewritten source instead.
A name WGSL will not accept — a keyword, an empty string, anything that is not an identifier — is ErrInvalidIdentifier, and is refused before the shader is even parsed.
So is a name outside ASCII. WGSL opens an identifier with any XID_Start rune, and the engine reads such names perfectly well — this operation will find and rename héllo — but it will not rename anything *to* wörld. That is a limit of the renamer rather than of the language.
func RenameByID ¶
RenameByID renames the symbol a StableID refers to, producing the same edits Rename would produce from an offset on that symbol.
The difference is when the ID was obtained: an offset taken before an unrelated edit points somewhere else afterwards, and an ID does not.
type EntryPoint ¶
type EntryPoint struct {
// Name is the function name. It is never renamed by minification, because
// the host names this function when it builds a pipeline.
Name string `json:"name"`
// NameOffset is the byte offset of the name in the original source.
NameOffset int `json:"nameOffset"`
// StableID names this function across reparses.
StableID StableID `json:"stableId,omitempty"`
// DeclSpan covers the whole function.
DeclSpan Span `json:"declSpan,omitzero"`
// Stage is which pipeline stage this is.
Stage ShaderStage `json:"stage"`
// WorkgroupSize is the @workgroup_size, nil on a vertex or fragment entry
// point. A dimension given by an override reads as 0, since its value is
// not known until the pipeline is created.
WorkgroupSize *[3]int `json:"workgroupSize"`
// Overrides names the pipeline-overridable constants this entry point
// depends on.
Overrides []string `json:"overrides,omitempty"`
// Inputs and Outputs are the stage's IO, with struct parameters and
// returns flattened to their members.
Inputs []IOVar `json:"inputs"`
Outputs []IOVar `json:"outputs"`
// Resources names the bindings this entry point reaches, directly or
// through the functions it calls.
Resources []string `json:"resources"`
}
An EntryPoint is one @compute, @vertex or @fragment function — a pipeline stage the host can name.
type Field ¶
type Field struct {
// Name is the member as written and NameMapped what it became after
// minification.
Name string `json:"name"`
NameMapped string `json:"nameMapped"`
// NameOffset is the byte offset of the name in the original source.
NameOffset int `json:"nameOffset"`
// StableID names this member across reparses.
StableID StableID `json:"stableId,omitempty"`
// TypeSpan covers the member's type in the original source.
TypeSpan Span `json:"typeSpan,omitzero"`
// Type is the type as written and TypeMapped what it became.
Type string `json:"type"`
TypeMapped string `json:"typeMapped"`
// Offset is the member's byte offset within the struct. It is not the
// running sum of the preceding sizes: alignment inserts padding.
Offset int `json:"offset"`
// Size and Alignment are the member's own.
Size int `json:"size"`
Alignment int `json:"alignment"`
// Layout is the nested layout, present when this member is itself a
// struct.
Layout *StructLayout `json:"layout,omitempty"`
// TypeInfo is the structured type.
TypeInfo *TypeInfo `json:"typeInfo,omitempty"`
}
A Field is one member of a StructLayout.
type Fix ¶
type Fix struct {
// Range is the span to replace.
Range Range
// Text is what to put there. Empty means a deletion.
Text string
}
A Fix is a rewrite that resolves a Diagnostic: replace the source in Range with Text.
type Function ¶
type Function struct {
// Name is the function as written and NameMapped what it became. The
// latter is empty unless there was a renaming pass.
Name string `json:"name"`
NameMapped string `json:"nameMapped,omitempty"`
// NameOffset is the byte offset of the name in the original source.
NameOffset int `json:"nameOffset"`
// StableID names this function across reparses.
StableID StableID `json:"stableId,omitempty"`
// DeclSpan covers the whole function.
DeclSpan Span `json:"declSpan,omitzero"`
// InUse reports whether some entry point can reach this function. It is
// false for everything in a source that did not parse, since nothing was
// resolved.
InUse bool `json:"inUse"`
// Calls names the functions this one calls directly.
Calls []string `json:"calls"`
// DirectResources and DirectOverrides name what this function's own body
// touches, without following calls. [EntryPoint.Resources] is the
// transitive version.
DirectResources []string `json:"directResources"`
DirectOverrides []string `json:"directOverrides"`
}
A Function is one node of the call graph. Entry points appear here too.
type IOVar ¶
type IOVar struct {
// Name is the identifier, empty for a return value — WGSL gives those an
// attribute rather than a name.
Name string `json:"name"`
// Location is the @location index, nil when this is a builtin.
Location *int `json:"location,omitempty"`
// Builtin is the @builtin name, empty when this has a location. Exactly
// one of the two is set.
Builtin string `json:"builtin,omitempty"`
// Interpolate is the @interpolate attribute, nil when there is none.
Interpolate *Interpolation `json:"interpolate,omitempty"`
// Type is the type as written.
Type string `json:"type,omitempty"`
// TypeInfo is the structured type.
TypeInfo *TypeInfo `json:"typeInfo,omitempty"`
}
An IOVar is one input to or output from an entry point: a parameter, a return value, or one member of a struct standing in for either.
type Interpolation ¶
type Interpolation struct {
// Type is "perspective", "linear" or "flat".
Type string `json:"type"`
// Sampling is "center", "centroid", "sample", "first" or "either", and is
// empty when the attribute did not give one.
Sampling string `json:"sampling,omitempty"`
}
An Interpolation is a @interpolate attribute: how a value is interpolated across a primitive, and where it is sampled.
type LintConfig ¶
type LintConfig struct {
// Extends lists shareable packs to start from, in order.
Extends []Pack `json:"extends,omitzero"`
// Rules overrides individual rules by id. An id no rule answers to is
// silently ignored — the engine does not report unknown rule names, so a
// typo here reads as a rule that never fires.
Rules map[string]RuleSetting `json:"rules,omitzero"`
// ReportUnusedDisableDirectives reports wgslender-disable comments that
// suppress nothing. Off by default.
ReportUnusedDisableDirectives Opt[bool] `json:"reportUnusedDisableDirectives,omitzero"`
}
A LintConfig says which rules to run, and how loudly.
The zero value runs **no rules at all** — the opposite of MinifyOptions, whose zero value means wgslender's own defaults. A nil *LintConfig means the same thing. Start from PackRecommended to get wgslender's opinion:
cfg := &wgslender.LintConfig{
Extends: []wgslender.Pack{wgslender.PackRecommended},
Rules: map[string]wgslender.RuleSetting{"no-magic-numbers": wgslender.RuleOff()},
}
Rules win over anything Extends said.
type LintFixOutcome ¶
type LintFixOutcome struct {
// Fixed is the source with every available autofix applied.
Fixed string
// Report describes the source **as it was handed in**, not Fixed. Run
// [Lint] on Fixed to see what is left.
Report LintReport
}
A LintFixOutcome is what LintFix produced.
func LintFix ¶
func LintFix(ctx context.Context, source string, cfg *LintConfig) (LintFixOutcome, error)
LintFix lints a shader and applies every autofix in one pass.
Rules whose diagnostics carry no fix are reported and left alone, so the returned source can still lint dirty. It is always valid WGSL, though: an autofix that broke the shader would be worse than no autofix at all.
Example ¶
ExampleLintFix applies every available autofix in one pass. The report it returns describes the source **as it was handed in**, not the fixed one — run Lint on the result to see what is left.
package main
import (
"context"
"fmt"
"log"
"github.com/HugoDaniel/wgslender/packages/go/wgslender"
)
func main() {
// The cast is redundant — i * 2u is already a u32 — and the rule that says
// so carries an autofix.
const shader = `@group(0) @binding(0) var<storage, read_write> counters: array<u32>;
@compute @workgroup_size(64)
fn main(@builtin(local_invocation_index) i: u32) {
let doubled = u32(i * 2u);
counters[i] = doubled;
}
`
cfg := &wgslender.LintConfig{Extends: []wgslender.Pack{wgslender.PackRecommended}}
outcome, err := wgslender.LintFix(context.Background(), shader, cfg)
if err != nil {
log.Fatal(err)
}
fmt.Println("fixable:", outcome.Report.FixableCount)
fmt.Print(outcome.Fixed)
}
Output: fixable: 1 @group(0) @binding(0) var<storage, read_write> counters: array<u32>; @compute @workgroup_size(64) fn main(@builtin(local_invocation_index) i: u32) { let doubled = i * 2u; counters[i] = doubled; }
type LintReport ¶
type LintReport struct {
// ErrorCount is validator errors plus lint errors.
ErrorCount int
// WarningCount is lint warnings only.
WarningCount int
// FixableCount is how many diagnostics carry an autofix [LintFix] can
// apply.
FixableCount int
// Diagnostics is everything found, the validator's first and then the
// linter's. Lint entries are the ones whose [Diagnostic.Source] is
// "wgslender-lint".
Diagnostics []Diagnostic
}
A LintReport is what Lint found.
The counts and the diagnostics do not add up, and that is the engine's shape rather than an oversight here: ErrorCount covers the validator's errors as well as the linter's, while WarningCount covers only the linter's — even though Diagnostics also carries the validator's warnings. Count the severities in Diagnostics yourself if you need a total.
func Lint ¶
func Lint(ctx context.Context, source string, cfg *LintConfig) (LintReport, error)
Lint runs the configured rules over a shader. A nil cfg runs no rules at all; see LintConfig.
Rule violations are not a Go error: they come back as a report. The returned error is reserved for a call that could not be made or trusted — see ErrInvalidUTF8, ErrSourceTooLarge and ErrInternal.
Example ¶
ExampleLint runs wgslender's recommended rules. The config is spelled out because a nil *LintConfig runs no rules at all — the engine has no default rule set, so wgslender's opinion has to be asked for by name.
package main
import (
"context"
"fmt"
"log"
"github.com/HugoDaniel/wgslender/packages/go/wgslender"
)
func main() {
const shader = `@group(0) @binding(0) var<storage, read_write> data: array<f32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3u) {
let unused = 3.14159;
data[id.x] = f32(id.x) * 2.0;
}
`
report, err := wgslender.Lint(context.Background(), shader, &wgslender.LintConfig{
Extends: []wgslender.Pack{wgslender.PackRecommended},
})
if err != nil {
log.Fatal(err)
}
fmt.Println("errors:", report.ErrorCount, "warnings:", report.WarningCount)
for _, d := range report.Diagnostics {
fmt.Printf("%d:%d %s: %s\n", d.Line, d.Column, d.Code, d.Message)
}
}
Output: errors: 0 warnings: 1 5:9 W0001: 'unused' is declared but never used
type MinifiedShader ¶
type MinifiedShader struct {
MinifyResult
// Reflection describes the *original* source, with each name's minified
// form alongside it in the NameMapped fields. That pairing is the reason
// to call this instead of [Minify] and [Reflect] separately: it is what
// lets a host look up the buffer layout it knows by its author's name and
// bind it under the name that survived.
Reflection Reflection
}
A MinifiedShader is what MinifyAndReflect produced: the minified shader, and a description of the interface it presents.
MinifyResult is embedded, so Code, Errors and the sizes are reached directly.
func MinifyAndReflect ¶
func MinifyAndReflect(ctx context.Context, source string, opts *MinifyOptions) (MinifiedShader, error)
MinifyAndReflect minifies a shader and reflects over it in one pass. A nil opts means wgslender's defaults; see MinifyOptions.
The two halves report parse failures independently — MinifyResult.Errors and Reflection.Errors — and say the same thing when they do.
Example ¶
ExampleMinifyAndReflect minifies and reflects in one pass. The reflection describes the *original* source with each name's minified form alongside, which is what lets a host keep talking about the shader in its author's vocabulary while binding whatever survived.
package main
import (
"context"
"fmt"
"log"
"github.com/HugoDaniel/wgslender/packages/go/wgslender"
)
// exampleShader is what the examples work on: two resources, a helper the
// minifier is free to rename, and one compute entry point.
const exampleShader = `struct Params {
resolution: vec2f,
time: f32,
}
@group(0) @binding(0) var<uniform> params: Params;
@group(1) @binding(2) var<storage, read_write> data: array<vec4f>;
fn luminance(color: vec3f) -> f32 {
return dot(color, vec3f(0.2126, 0.7152, 0.0722));
}
@compute @workgroup_size(8, 8, 1)
fn main(@builtin(global_invocation_id) id: vec3u) {
let uv = vec2f(id.xy) / params.resolution;
let lum = luminance(vec3f(uv, params.time));
data[id.x] = vec4f(lum, lum, lum, 1.0);
}
`
func main() {
shader, err := wgslender.MinifyAndReflect(context.Background(), exampleShader, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(shader.OriginalSize, "bytes ->", shader.MinifiedSize)
for _, b := range shader.Reflection.Bindings {
fmt.Printf("%s: %s -> %s: %s\n", b.Name, b.Type, b.NameMapped, b.TypeMapped)
}
// The binding names survive — the host binds against them — but the
// struct type behind params is internal to the shader, so Params is now f.
}
Output: 497 bytes -> 375 params: Params -> params: f data: array<vec4f> -> data: array<vec4f>
type MinifyOptions ¶
type MinifyOptions struct {
// MinifyWhitespace strips whitespace and comments. On by default.
MinifyWhitespace Opt[bool] `json:"minifyWhitespace,omitzero"`
// MinifyIdentifiers renames identifiers to short names. On by default.
MinifyIdentifiers Opt[bool] `json:"minifyIdentifiers,omitzero"`
// MinifySyntax rewrites syntax into shorter equivalent forms. On by
// default.
MinifySyntax Opt[bool] `json:"minifySyntax,omitzero"`
// TreeShaking drops declarations no entry point can reach. On by default.
TreeShaking Opt[bool] `json:"treeShaking,omitzero"`
// MangleExternalBindings renames @group/@binding variables too. Off by
// default, because renaming them changes the names a host program binds
// against.
MangleExternalBindings Opt[bool] `json:"mangleExternalBindings,omitzero"`
// PreserveUniformStructTypes keeps the type names of uniform and storage
// structs intact. Off by default.
PreserveUniformStructTypes Opt[bool] `json:"preserveUniformStructTypes,omitzero"`
// KeepNames lists identifiers that must never be renamed. Names the shader
// does not declare are ignored.
KeepNames []string `json:"keepNames,omitzero"`
// SortDeclarations groups similar module-level declarations together,
// which compresses better. Off by default.
SortDeclarations Opt[bool] `json:"sortDeclarations,omitzero"`
// ScopeLocalRename reuses the same short names across sibling scopes,
// which compresses better. Off by default.
ScopeLocalRename Opt[bool] `json:"scopeLocalRename,omitzero"`
// SourceMap asks for a source map alongside the minified output, returned
// in [MinifyResult.SourceMap]. Off by default.
SourceMap Opt[bool] `json:"sourceMap,omitzero"`
// SourceMapSources embeds the original source text in that source map. Off
// by default, and only meaningful with SourceMap set.
SourceMapSources Opt[bool] `json:"sourceMapSources,omitzero"`
}
MinifyOptions overrides wgslender's minification defaults.
Every field is absent by default, so the zero value means *no overrides — wgslender's own defaults apply*, not *everything off*. The defaults are: MinifyOptions.MinifyWhitespace, MinifyOptions.MinifyIdentifiers, MinifyOptions.MinifySyntax and MinifyOptions.TreeShaking on, everything else off, no kept names.
A nil *MinifyOptions means the same thing, so a caller with nothing to say passes nil.
Note that LintConfig, the other options type in this package, reads its zero value the opposite way: no rules at all. The asymmetry is the engine's, not this package's.
type MinifyResult ¶
type MinifyResult struct {
// Code is the minified shader, or the original source verbatim if it did
// not parse.
Code string
// Errors describes what stopped the minifier from doing better. It is
// populated only by parse failures: a shader that parses but does not
// type-check minifies normally and reports nothing here. Use [Validate] to
// find out whether a shader is correct.
Errors []string
// OriginalSize is the byte length of the input.
OriginalSize int
// MinifiedSize is the byte length of Code.
MinifiedSize int
// SourceMap is a v3 source map, present only when
// [MinifyOptions.SourceMap] asked for one. It stays raw JSON: the format
// is standardised elsewhere and callers usually want to write it straight
// to a file.
SourceMap json.RawMessage
}
A MinifyResult is the outcome of minifying one shader.
It is a report, not a success-or-failure: a shader the parser cannot read comes back with Code equal to the input and Errors describing why, because silently returning an empty shader would be worse than returning the one the caller already had.
func Minify ¶
func Minify(ctx context.Context, source string, opts *MinifyOptions) (MinifyResult, error)
Minify shrinks a WGSL shader. A nil opts means wgslender's defaults; see MinifyOptions.
The returned error reports a call that could not be made or trusted — source that is not valid UTF-8 (ErrInvalidUTF8), too large for the engine (ErrSourceTooLarge), an engine failure (ErrInternal) — and never a problem with the shader itself. Shader problems are in MinifyResult.Errors, and a shader that does not parse comes back unchanged rather than empty.
Example ¶
ExampleMinify shortens a shader with wgslender's own default pipeline, which a nil *MinifyOptions asks for.
package main
import (
"context"
"fmt"
"log"
"github.com/HugoDaniel/wgslender/packages/go/wgslender"
)
// exampleShader is what the examples work on: two resources, a helper the
// minifier is free to rename, and one compute entry point.
const exampleShader = `struct Params {
resolution: vec2f,
time: f32,
}
@group(0) @binding(0) var<uniform> params: Params;
@group(1) @binding(2) var<storage, read_write> data: array<vec4f>;
fn luminance(color: vec3f) -> f32 {
return dot(color, vec3f(0.2126, 0.7152, 0.0722));
}
@compute @workgroup_size(8, 8, 1)
fn main(@builtin(global_invocation_id) id: vec3u) {
let uv = vec2f(id.xy) / params.resolution;
let lum = luminance(vec3f(uv, params.time));
data[id.x] = vec4f(lum, lum, lum, 1.0);
}
`
func main() {
result, err := wgslender.Minify(context.Background(), exampleShader, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.OriginalSize, "bytes ->", result.MinifiedSize)
fmt.Println(result.Code)
// The @group/@binding variables keep their names: the host binds against
// them, so renaming them would break the pipeline that uses this shader.
// Everything else is fair game.
}
Output: 497 bytes -> 375 struct f{resolution:vec2f,time:f32}@group(0) @binding(0) var<uniform> params:f;@group(1) @binding(2) var<storage,read_write> data:array<vec4f>;fn c(d:vec3f)->f32{return dot(d,vec3f(.2126,.7152,.0722));}@compute @workgroup_size(8,8,1) fn main(@builtin(global_invocation_id) b:vec3u){let e=vec2f(b.xy)/params.resolution;let a=c(vec3f(e,params.time));data[b.x]=vec4f(a,a,a,1.);}
Example (Options) ¶
ExampleMinify_options overrides the defaults where a caller most often needs to: KeepNames pins an identifier a host looks up by name, and the two compression flags reorder and rename for DEFLATE's benefit — the output is no shorter, but it compresses better on the wire. The fields are Opt[bool], so anything not Set stays whatever wgslender decides.
package main
import (
"context"
"fmt"
"log"
"github.com/HugoDaniel/wgslender/packages/go/wgslender"
)
// exampleShader is what the examples work on: two resources, a helper the
// minifier is free to rename, and one compute entry point.
const exampleShader = `struct Params {
resolution: vec2f,
time: f32,
}
@group(0) @binding(0) var<uniform> params: Params;
@group(1) @binding(2) var<storage, read_write> data: array<vec4f>;
fn luminance(color: vec3f) -> f32 {
return dot(color, vec3f(0.2126, 0.7152, 0.0722));
}
@compute @workgroup_size(8, 8, 1)
fn main(@builtin(global_invocation_id) id: vec3u) {
let uv = vec2f(id.xy) / params.resolution;
let lum = luminance(vec3f(uv, params.time));
data[id.x] = vec4f(lum, lum, lum, 1.0);
}
`
func main() {
opts := &wgslender.MinifyOptions{
KeepNames: []string{"luminance"},
SortDeclarations: wgslender.Set(true),
ScopeLocalRename: wgslender.Set(true),
}
result, err := wgslender.Minify(context.Background(), exampleShader, opts)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Code)
// luminance kept its name, and the short names restart in every scope —
// both functions open with a — which is ScopeLocalRename making the bytes
// repeat for the compressor.
}
Output: struct e{resolution:vec2f,time:f32}@group(0) @binding(0) var<uniform> params:e;@group(1) @binding(2) var<storage,read_write> data:array<vec4f>;fn luminance(a:vec3f)->f32{return dot(a,vec3f(.2126,.7152,.0722));}@compute @workgroup_size(8,8,1) fn main(@builtin(global_invocation_id) a:vec3u){let b=vec2f(a.xy)/params.resolution;let c=luminance(vec3f(b,params.time));data[a.x]=vec4f(c,c,c,1.);}
type Opt ¶
type Opt[T any] struct { // contains filtered or unexported fields }
An Opt holds a value that may not have been given.
It exists because the engine's option objects are override sets: every key is absent by default, and an absent key is not the same as a false one. Absent means "keep wgslender's default"; false means "turn this off". A plain bool field cannot say the first thing, and a *bool says it at the cost of making every option a pointer to write and a nil check to read.
The zero Opt is the absent one, which is what makes the surrounding options structs useful at their zero value.
Encoding one is only meaningful through a struct field tagged omitzero:
MinifyWhitespace Opt[bool] `json:"minifyWhitespace,omitzero"`
omitzero consults Opt.IsZero, so an unset field disappears from the document rather than encoding as null.
func (Opt[T]) Get ¶
Get returns the value and whether it was ever set. The value is T's zero value when it was not.
func (Opt[T]) IsZero ¶
IsZero reports whether the value is absent. encoding/json calls it for omitzero fields.
func (Opt[T]) MarshalJSON ¶
MarshalJSON encodes a set value alone, and an absent Opt as null. A field holding an absent Opt should be tagged omitzero so the null is never reached — this package's own fields all are — but a field that insists on being present says "nothing" the way JSON spells it, not as T's zero value passing for a choice.
func (*Opt[T]) UnmarshalJSON ¶
UnmarshalJSON decodes a present value, marking it set, and reads null as absence — whatever the Opt held before. A key that never appears in the document also leaves the Opt absent, because Unmarshal does not call this at all for a missing key.
type Override ¶
type Override struct {
// Name is the identifier as written and NameMapped what it became.
Name string `json:"name"`
NameMapped string `json:"nameMapped"`
// NameOffset is the byte offset of the name in the original source.
NameOffset int `json:"nameOffset"`
// StableID names this constant across reparses.
StableID StableID `json:"stableId,omitempty"`
// DeclSpan covers the whole declaration.
DeclSpan Span `json:"declSpan,omitzero"`
// ID is the @id, nil when the declaration has none. @id(0) is legal and
// means something different from no @id at all, which is why this is a
// pointer.
ID *int `json:"id"`
// Type is the declared type.
Type string `json:"type,omitempty"`
// TypeInfo is the structured type.
TypeInfo *TypeInfo `json:"typeInfo,omitempty"`
// Default is the default value as an *expression*, not a number: an
// override declared `= 8u` reads back as "8u", suffix and all.
Default string `json:"default,omitempty"`
}
An Override is a pipeline-overridable constant — a value the host may replace at pipeline-creation time.
type Pack ¶
type Pack string
A Pack is a shareable rule set, the equivalent of an extends entry in wgslender.json.
const ( // PackRecommended is the default set: rules that catch likely mistakes. PackRecommended Pack = "@wgslender/recommended" // PackStyle covers formatting and naming conventions. PackStyle Pack = "@wgslender/style" // PackPerformance covers rules about shader cost. PackPerformance Pack = "@wgslender/performance" // PackPortability covers rules about running on more backends. PackPortability Pack = "@wgslender/portability" // PackMinify covers rules that make a shader minify better. PackMinify Pack = "@wgslender/minify" // PackStrict is everything, at error severity. PackStrict Pack = "@wgslender/strict" )
type Position ¶
type Position struct {
// Line is 1-based.
Line int
// Column is 1-based.
Column int
// Offset is a 0-based UTF-8 byte offset from the start of the source.
Offset int
}
A Position is one place in a source file, given three ways because different callers want different ones: editors want line and column, splicing wants the offset.
type Range ¶
type Range struct {
// Start is where the span begins.
Start Position
// End is the first position after it.
End Position
}
A Range is a half-open span of source, from Start up to but not including End.
type Reference ¶
type Reference struct {
// Span is the half-open byte range of the identifier, and covers the name
// alone rather than the expression it appears in.
Span
// IsWrite reports whether this mention writes the symbol. The declaration
// is a write; so is the left-hand side of an assignment.
IsWrite bool `json:"isWrite"`
}
A Reference is one mention of a symbol.
func FindReferences ¶
func FindReferences(ctx context.Context, source string, offset int, d Declarations) ([]Reference, error)
FindReferences finds every mention of the symbol at a byte offset.
The result is in source order, so the declaration — when it is included — comes first. An offset that names no symbol is not an error here: it comes back as no references at all, which lets an editor ask about wherever the cursor happens to be without checking first. Every other operation in this family calls the same situation ErrSymbolNotFound.
Example ¶
ExampleFindReferences lists every mention of a symbol, given any byte offset inside one of them. Offsets are plain string indexes, so strings.Index is all it takes to point at a name.
package main
import (
"context"
"fmt"
"log"
"strings"
"github.com/HugoDaniel/wgslender/packages/go/wgslender"
)
// exampleShader is what the examples work on: two resources, a helper the
// minifier is free to rename, and one compute entry point.
const exampleShader = `struct Params {
resolution: vec2f,
time: f32,
}
@group(0) @binding(0) var<uniform> params: Params;
@group(1) @binding(2) var<storage, read_write> data: array<vec4f>;
fn luminance(color: vec3f) -> f32 {
return dot(color, vec3f(0.2126, 0.7152, 0.0722));
}
@compute @workgroup_size(8, 8, 1)
fn main(@builtin(global_invocation_id) id: vec3u) {
let uv = vec2f(id.xy) / params.resolution;
let lum = luminance(vec3f(uv, params.time));
data[id.x] = vec4f(lum, lum, lum, 1.0);
}
`
func main() {
off := strings.Index(exampleShader, "luminance")
refs, err := wgslender.FindReferences(context.Background(), exampleShader, off, wgslender.WithDeclaration)
if err != nil {
log.Fatal(err)
}
for _, r := range refs {
kind := "read"
if r.IsWrite {
kind = "write"
}
fmt.Printf("%s at %d..%d (%s)\n", exampleShader[r.Start:r.End], r.Start, r.End, kind)
}
// The declaration comes first and is the write; the call site is the read.
}
Output: luminance at 179..188 (write) luminance at 416..425 (read)
type Reflection ¶
type Reflection struct {
// Version is the schema version of the engine's document. It is 2.
Version int `json:"version"`
// Bindings is every @group/@binding variable, in declaration order.
Bindings []Binding `json:"bindings"`
// Uniforms, Storage, Textures and Samplers are views of Bindings by kind.
// They hold whole copies, not indices, so a binding read out of one of
// them is complete.
Uniforms []Binding `json:"uniforms"`
Storage []Binding `json:"storage"`
Textures []Binding `json:"textures"`
Samplers []Binding `json:"samplers"`
// Structs maps each declared struct's name to its memory layout.
Structs map[string]StructLayout `json:"structs"`
// EntryPoints is every @compute, @vertex and @fragment function.
EntryPoints []EntryPoint `json:"entryPoints"`
// Overrides is every pipeline-overridable constant.
Overrides []Override `json:"overrides"`
// Functions is the call graph — every function including the entry
// points. It is populated even for source that did not parse, which is
// what lets an editor keep showing an outline of a broken file.
Functions []Function `json:"functions"`
// Aliases is every type alias.
Aliases []Alias `json:"aliases"`
// Errors is what stopped the parser, and is empty for anything that
// parsed. A shader that parses but does not type-check reflects cleanly
// and reports nothing here; use [Validate] to find out whether it is
// correct.
Errors []string `json:"errors,omitempty"`
}
A Reflection is everything the engine can say about a shader's interface: what it binds, what it declares and how it is laid out in memory.
Every collection is present whether or not it holds anything. Errors is the exception and the useful one — it is non-empty exactly when the source did not parse.
func Reflect ¶
func Reflect(ctx context.Context, source string) (Reflection, error)
Reflect describes a shader's interface: its bindings, structs, entry points, overrides, aliases and call graph. See Reflection.
A shader that does not parse is not a Go error: what the parser managed to read comes back, with the complaints in Reflection.Errors. The returned error is reserved for a call that could not be made or trusted — see ErrInvalidUTF8, ErrSourceTooLarge and ErrInternal.
Example ¶
ExampleReflect reads a shader's interface: what it binds, what stages it offers, and how a struct is laid out in memory.
package main
import (
"context"
"fmt"
"log"
"github.com/HugoDaniel/wgslender/packages/go/wgslender"
)
// exampleShader is what the examples work on: two resources, a helper the
// minifier is free to rename, and one compute entry point.
const exampleShader = `struct Params {
resolution: vec2f,
time: f32,
}
@group(0) @binding(0) var<uniform> params: Params;
@group(1) @binding(2) var<storage, read_write> data: array<vec4f>;
fn luminance(color: vec3f) -> f32 {
return dot(color, vec3f(0.2126, 0.7152, 0.0722));
}
@compute @workgroup_size(8, 8, 1)
fn main(@builtin(global_invocation_id) id: vec3u) {
let uv = vec2f(id.xy) / params.resolution;
let lum = luminance(vec3f(uv, params.time));
data[id.x] = vec4f(lum, lum, lum, 1.0);
}
`
func main() {
r, err := wgslender.Reflect(context.Background(), exampleShader)
if err != nil {
log.Fatal(err)
}
for _, b := range r.Bindings {
fmt.Printf("@group(%d) @binding(%d) %s: %s\n", b.Group, b.Binding, b.Name, b.Type)
}
for _, e := range r.EntryPoints {
fmt.Printf("%s entry point %q, workgroup %v\n", e.Stage, e.Name, *e.WorkgroupSize)
}
// A struct's layout is what a host needs in order to write its buffer:
// where each member begins, and how long the whole thing is.
params := r.Structs["Params"]
fmt.Printf("struct Params is %d bytes\n", params.Size)
for _, f := range params.Fields {
fmt.Printf(" %s: %s at %d\n", f.Name, f.Type, f.Offset)
}
}
Output: @group(0) @binding(0) params: Params @group(1) @binding(2) data: array<vec4f> compute entry point "main", workgroup [8 8 1] struct Params is 16 bytes resolution: vec2f at 0 time: f32 at 8
type RelatedInfo ¶
type RelatedInfo struct {
// Line is the 1-based line.
Line int
// Column is the 1-based column.
Column int
// Message says why this place is relevant.
Message string
}
A RelatedInfo is a second place in the source that explains a Diagnostic.
type RuleSetting ¶
type RuleSetting struct {
// contains filtered or unexported fields
}
A RuleSetting says what one lint rule should do. Build one with RuleOff, RuleWarn, RuleError, RuleWarnWith or RuleErrorWith; the zero value is RuleOff.
func RuleError ¶
func RuleError() RuleSetting
RuleError runs the rule, reporting at error severity.
Reporting at error severity is a statement about the shader, not about the call: Lint still returns a report rather than a Go error.
func RuleErrorWith ¶
func RuleErrorWith(opts map[string]any) RuleSetting
RuleErrorWith runs the rule at error severity with its own options. A nil map means the same as RuleError.
func RuleOff ¶
func RuleOff() RuleSetting
RuleOff does not run the rule, overriding whatever a pack said about it.
func RuleWarnWith ¶
func RuleWarnWith(opts map[string]any) RuleSetting
RuleWarnWith runs the rule at warning severity with its own options. A nil map means the same as RuleWarn.
func (RuleSetting) MarshalJSON ¶
func (r RuleSetting) MarshalJSON() ([]byte, error)
MarshalJSON writes the two forms the engine reads: a bare severity word, or a ["warn", {…}] pair when the rule was given options.
type Severity ¶
type Severity string
A Severity says how seriously to take a Diagnostic.
It is deliberately an open enum: the constants below are what the engine spells today, but a severity this package has never heard of arrives verbatim rather than being flattened into one of them. Compare against the constants, and expect the default branch to be reachable.
const ( // SeverityError rejects the shader. SeverityError Severity = "error" // SeverityWarning is legal, but almost certainly not what the author meant. SeverityWarning Severity = "warning" // SeverityInfo is neutral commentary. SeverityInfo Severity = "info" // SeverityNote is extra context attached to another diagnostic. SeverityNote Severity = "note" // SeverityHint is a suggestion an editor can act on. SeverityHint Severity = "hint" // SeverityUnknown is what the engine falls back to when it has no better // word. It is a real wire value, not this package's fallback. SeverityUnknown Severity = "unknown" )
type ShaderStage ¶
type ShaderStage string
A ShaderStage is the pipeline stage an entry point belongs to.
const ( // StageCompute runs over a workgroup grid the host dispatches. StageCompute ShaderStage = "compute" // StageVertex runs once per vertex. StageVertex ShaderStage = "vertex" // StageFragment runs once per rasterised fragment. StageFragment ShaderStage = "fragment" )
type Span ¶
A Span is a half-open byte range of source, from Start up to but not including End. Offsets are UTF-8 bytes, which is what Go's own string indexing uses, so source[s.Start:s.End] is the text it covers.
In reflection the zero Span means the engine did not record one: it omits a span it could not determine, and its own test for presence is End > Start, so no real span is ever zero. Elsewhere absence is reported some other way — the locate operations use a second return — and a zero Span is not meaningful on its own.
func LocateDeclaration ¶
LocateDeclaration finds the whole declaration a StableID refers to: attributes, body, trailing semicolon and all. It is the span to remove, or to fold, or to jump to.
A struct member has no declaration of its own in this sense, so it comes back false even though LocateStableID and LocateType both resolve it.
func LocateStableID ¶
LocateStableID finds the name a StableID refers to.
The span covers the identifier alone. The false return means this source does not contain that symbol — it was deleted, or the ID came from a different file, or it is in a format this engine does not speak — and is an answer rather than a failure.
func LocateType ¶
LocateType finds the type annotation a StableID refers to: a function's return type, a parameter's or member's declared type, a variable's annotation when it has one.
The false return means there is nothing to point at — an entry point with no return type, a let whose type is inferred — which is exactly the case ChangeType reports as ErrNoTypeAnnotation.
type StableID ¶
type StableID string
A StableID names a symbol in a way that survives reparsing, such as "v1:fn:main/block#0/let:y". It is what the refactor operations take instead of a byte offset, which moves the moment anyone edits the file.
func StableIDAtOffset ¶
StableIDAtOffset names the symbol at a byte offset in a way that survives reparsing.
The false return is for an offset that names no symbol — a comment, an operator, past the end of the file — which is an answer and not a failure.
Not everything with an ID has one that can be found this way. A struct member's ID works everywhere it is accepted, but only Reflect hands it out; asking for one at the member's own offset comes back false.
Example ¶
ExampleStableIDAtOffset shows what a StableID is for. A byte offset names a symbol only until the next edit moves it; the ID keeps naming it across any edit that leaves the declaration in place.
package main
import (
"context"
"fmt"
"log"
"strings"
"github.com/HugoDaniel/wgslender/packages/go/wgslender"
)
// exampleShader is what the examples work on: two resources, a helper the
// minifier is free to rename, and one compute entry point.
const exampleShader = `struct Params {
resolution: vec2f,
time: f32,
}
@group(0) @binding(0) var<uniform> params: Params;
@group(1) @binding(2) var<storage, read_write> data: array<vec4f>;
fn luminance(color: vec3f) -> f32 {
return dot(color, vec3f(0.2126, 0.7152, 0.0722));
}
@compute @workgroup_size(8, 8, 1)
fn main(@builtin(global_invocation_id) id: vec3u) {
let uv = vec2f(id.xy) / params.resolution;
let lum = luminance(vec3f(uv, params.time));
data[id.x] = vec4f(lum, lum, lum, 1.0);
}
`
func main() {
ctx := context.Background()
id, ok, err := wgslender.StableIDAtOffset(ctx, exampleShader, strings.Index(exampleShader, "luminance"))
if err != nil {
log.Fatal(err)
}
if !ok {
log.Fatal("no symbol at that offset")
}
fmt.Println(id)
// A leading comment moves every byte in the file. The offset the ID was
// taken at now points into the comment; the ID still finds the symbol.
edited := "// tone mapping helpers\n" + exampleShader
span, ok, err := wgslender.LocateStableID(ctx, edited, id)
if err != nil {
log.Fatal(err)
}
if !ok {
log.Fatal("the symbol is gone")
}
fmt.Printf("%s at %d..%d\n", edited[span.Start:span.End], span.Start, span.End)
// The span is the FindReferences declaration span from the example above,
// moved by exactly the length of the comment.
}
Output: v1:fn:luminance luminance at 203..212
type Strictness ¶
type Strictness int
Strictness says whether Validate tolerates warnings.
It is a named choice rather than a bool argument, so that the call site says which mode it means.
const ( // DefaultStrictness leaves warnings as warnings, and a shader with nothing // worse than warnings is valid. DefaultStrictness Strictness = iota // Strict promotes every warning to an error, so any diagnostic at all // rejects the shader. Strict )
type StructLayout ¶
type StructLayout struct {
// Size is the struct's byte size, padding included.
Size int `json:"size"`
// Alignment is its byte alignment.
Alignment int `json:"alignment"`
// Fields is its members in declaration order.
Fields []Field `json:"fields"`
}
A StructLayout is a struct's size and field placement under WGSL's host-shareable layout rules (§6.2.10). It is what a host needs to write the buffer.
type TextureDimension ¶
type TextureDimension string
A TextureDimension is a texture's shape.
const ( // Dim1D is a one-dimensional texture. Dim1D TextureDimension = "1d" // Dim2D is a two-dimensional texture. Dim2D TextureDimension = "2d" // Dim2DArray is an array of two-dimensional layers. Dim2DArray TextureDimension = "2d_array" // Dim3D is a volume texture. Dim3D TextureDimension = "3d" // DimCube is a cube of six square faces. DimCube TextureDimension = "cube" // DimCubeArray is an array of cubes. DimCubeArray TextureDimension = "cube_array" )
type TextureKind ¶
type TextureKind string
A TextureKind is what a texture is for, which decides how it may be sampled and which bind-group layout entry it needs.
const ( // TextureSampled is read through a sampler. TextureSampled TextureKind = "sampled" // TextureMultisampled holds several samples per texel. TextureMultisampled TextureKind = "multisampled" // TextureStorage is read and written directly, no sampler involved. TextureStorage TextureKind = "storage" // TextureDepth holds depth values for comparison sampling. TextureDepth TextureKind = "depth" // TextureDepthMultisampled is a depth texture with several samples per // texel. TextureDepthMultisampled TextureKind = "depth_multisampled" // TextureExternal wraps a video frame the host imports. TextureExternal TextureKind = "external" )
type TypeInfo ¶
type TypeInfo struct {
// Kind discriminates everything else here.
Kind TypeKind `json:"kind"`
// Name is the type's name, on a scalar ("f32") or a struct ("Params").
// A struct's members are not here — look the name up in
// [Reflection.Structs].
Name string `json:"name,omitempty"`
// Width is a vector's component count; Cols and Rows are a matrix's
// shape.
Width int `json:"width,omitempty"`
Cols int `json:"cols,omitempty"`
Rows int `json:"rows,omitempty"`
// Format is the element type of a vector, matrix, array, atomic or
// pointer. A texture's format is a plain string and lives in TexFormat
// instead, even though the engine sends both under the same key.
Format *TypeInfo `json:"-"`
// Count is an array's element count, nil for a runtime-sized array.
Count *int `json:"count,omitempty"`
// Size is the byte size, nil only for a runtime-sized array. Samplers and
// textures have none.
Size *int `json:"size,omitempty"`
// Alignment is the byte alignment.
Alignment int `json:"alignment,omitempty"`
// Stride is the byte distance between a matrix's columns or an array's
// elements.
Stride int `json:"stride,omitempty"`
// Comparison distinguishes a sampler_comparison from a plain sampler.
Comparison bool `json:"comparison,omitempty"`
// Dim, TexKind, TexFormat and SampleType describe a texture. TexFormat is
// the storage format ("rgba8unorm") and is set only on storage textures;
// SampleType ("f32", "u32", …) is set only on sampled ones.
Dim TextureDimension `json:"dim,omitempty"`
TexKind TextureKind `json:"texKind,omitempty"`
TexFormat string `json:"-"`
SampleType string `json:"sampleType,omitempty"`
// Access is a storage texture's or a pointer's access mode.
Access AccessMode `json:"access,omitempty"`
// AddressSpace is a pointer's address space.
AddressSpace AddressSpace `json:"addressSpace,omitempty"`
}
A TypeInfo is a WGSL type, taken apart.
It is one struct for nine kinds of type, and TypeInfo.Kind says which fields mean anything — switch on it rather than testing fields for emptiness. The fields are grouped below by the kinds that use them.
func (*TypeInfo) UnmarshalJSON ¶
UnmarshalJSON decodes a type, resolving the one place the engine's document is ambiguous: "format" is a nested type on a vector, matrix, array, atomic or pointer, and a plain format name on a texture. Kind says which, so this dispatches on it rather than guessing from the JSON.
type TypeKind ¶
type TypeKind string
A TypeKind discriminates a TypeInfo. It says which of that struct's fields mean anything.
const ( // KindScalar is a lone numeric or boolean value. KindScalar TypeKind = "scalar" // KindVec is a vector of scalars. KindVec TypeKind = "vec" // KindMat is a matrix of column vectors. KindMat TypeKind = "mat" // KindArray is an array, sized or runtime-sized. KindArray TypeKind = "array" // KindStruct is a structure the shader declares. KindStruct TypeKind = "struct" // KindAtomic is an atomic wrapper over an integer scalar. KindAtomic TypeKind = "atomic" // KindSampler is a sampler, which has no memory layout at all. KindSampler TypeKind = "sampler" // KindTexture is a texture; [TextureKind] and [TextureDimension] say more. KindTexture TypeKind = "texture" // KindPtr is a pointer, which never crosses the host boundary. KindPtr TypeKind = "ptr" )
type Validation ¶
type Validation struct {
// Valid reports whether the shader is accepted. It is false whenever
// ErrorCount is non-zero.
Valid bool
// ErrorCount is the number of error-severity diagnostics.
ErrorCount int
// WarningCount is the number of warning-severity diagnostics. Under
// [Strict] it is always zero, because every warning became an error.
WarningCount int
// Diagnostics is everything the validator has to say, in source order.
Diagnostics []Diagnostic
}
A Validation is what Validate found.
func Validate ¶
func Validate(ctx context.Context, source string, s Strictness) (Validation, error)
Validate type-checks a WGSL shader.
A shader that fails validation is not a Go error: it comes back with Valid false and the diagnostics that explain why. The returned error is reserved for a call that could not be made or trusted — see ErrInvalidUTF8, ErrSourceTooLarge and ErrInternal.
Example ¶
ExampleValidate type-checks a shader that does not.
package main
import (
"context"
"fmt"
"log"
"github.com/HugoDaniel/wgslender/packages/go/wgslender"
)
func main() {
// A shader's own problems are data, not errors: the call succeeds and the
// verdict is in the result. A returned error means the call could not be
// made at all.
const broken = `@compute @workgroup_size(1)
fn main() {
let x: f32 = undeclared;
}
`
v, err := wgslender.Validate(context.Background(), broken, wgslender.DefaultStrictness)
if err != nil {
log.Fatal(err)
}
fmt.Println("valid:", v.Valid)
for _, d := range v.Diagnostics {
fmt.Printf("%d:%d %s[%s]: %s\n", d.Line, d.Column, d.Severity, d.Code, d.Message)
}
}
Output: valid: false 3:18 error[E0100]: use of undeclared identifier 'undeclared'