Writing a WASM Plugin¶
This guide walks through building a WASM plugin for Language Check using Go and TinyGo. The same principles apply to any language with an Extism PDK.
Overview¶
Language Check loads WASM plugins via Extism. Each plugin exports a check function that receives prose text and returns diagnostics. Plugins run in a sandboxed WebAssembly environment — they cannot access the filesystem, network, or host memory.
Protocol¶
Input¶
Your check function receives a JSON string:
{
"text": "The document text to check.",
"language_id": "markdown"
}
Output¶
Return a JSON string — an array of diagnostics:
[
{
"start_byte": 0,
"end_byte": 11,
"message": "Wordy phrase: consider using \"to\" instead",
"suggestions": ["to"],
"rule_id": "wordiness",
"severity": 1,
"confidence": 0.8
}
]
Field |
Type |
Required |
Description |
|---|---|---|---|
|
integer |
yes |
Start byte offset in the input text |
|
integer |
yes |
End byte offset (exclusive) |
|
string |
yes |
Human-readable description of the issue |
|
string[] |
no |
Replacement suggestions (first is preferred) |
|
string |
no |
Rule identifier (namespaced as |
|
integer |
no |
1 = Information, 2 = Warning (default), 3 = Error |
|
float |
no |
0.0–1.0, defaults to 0.7 if omitted |
Diagnostics from plugins are treated identically to built-in engine results — suggestions automatically appear in the SpeedFix panel, inlay hints, inline ghost text completions, and code actions. The first suggestion in the array is used as the preferred quick-fix. An empty string "" suggestion means “remove the matched text”, and suggestions prefixed with Insert "..." insert text at the diagnostic end position rather than replacing.
Setting Up¶
Prerequisites¶
Project Structure¶
plugins/my-checker/
├── detect.go # Detection logic (testable with standard Go)
├── detect_test.go # Unit tests
├── main.go # Extism PDK glue (build-tagged for TinyGo)
├── go.mod
├── go.sum
├── Makefile
└── my-checker.wasm # Built artifact
Writing the Plugin¶
Step 1: Define Types¶
Create detect.go with your data types and detection logic:
package main
type CheckRequest struct {
Text string `json:"text"`
LanguageID string `json:"language_id"`
}
type Diagnostic struct {
StartByte int `json:"start_byte"`
EndByte int `json:"end_byte"`
Message string `json:"message"`
Suggestions []string `json:"suggestions"`
RuleID string `json:"rule_id"`
Severity int `json:"severity"`
Confidence float32 `json:"confidence"`
}
Step 2: Implement Detection¶
Add your checking logic to detect.go. Keep it in a pure function that takes a string and returns diagnostics:
func FindIssues(text string) []Diagnostic {
// Your detection logic here
return diagnostics
}
This separation is important — detect.go uses only standard library imports and can be tested with go test.
Step 3: Wire Up Extism¶
Create main.go with the Extism PDK glue. Use a //go:build tinygo tag so standard Go ignores it during testing:
//go:build tinygo
package main
import (
"encoding/json"
"github.com/extism/go-pdk"
)
//export check
func check() int32 {
input := pdk.Input()
var req CheckRequest
if err := json.Unmarshal(input, &req); err != nil {
pdk.SetError(err)
return 1
}
diagnostics := FindIssues(req.Text)
output, err := json.Marshal(diagnostics)
if err != nil {
pdk.SetError(err)
return 1
}
pdk.Output(output)
return 0
}
func main() {}
Key points:
The
//export checkdirective exposes the function to the hostpdk.Input()reads the JSON input from the hostpdk.Output()writes the JSON response backpdk.SetError()reports errors to the hostReturn
0for success, non-zero for failurefunc main() {}is required but unused
Step 4: Initialize the Module¶
go mod init github.com/you/my-checker
go mod tidy
Testing¶
Unit Tests¶
Write tests in detect_test.go that exercise FindIssues() directly:
package main
import "testing"
func TestFindsIssue(t *testing.T) {
diags := FindIssues("some problematic text")
if len(diags) != 1 {
t.Fatalf("expected 1 diagnostic, got %d", len(diags))
}
}
Run with standard Go (no TinyGo needed):
go test -v ./...
Building¶
tinygo build -o my-checker.wasm -target wasi .
Manual Integration Test¶
Create a test config and run the CLI:
# .languagecheck.yaml
engines:
wasm_plugins:
- name: my-checker
path: plugins/my-checker/my-checker.wasm
echo "some problematic text" | language-check --stdin --language markdown
Integrating with Language Check¶
Explicit Configuration¶
Register your plugin in .languagecheck.yaml:
engines:
wasm_plugins:
- name: my-checker
path: path/to/my-checker.wasm
extensions: [md, txt] # optional: limit to specific file types
Auto-Discovery¶
Place the .wasm file in the auto-discovery directory:
.languagecheck/plugins/my-checker.wasm
The filename (minus .wasm) becomes the plugin name.
Debugging¶
Extism errors: The host logs plugin errors at
warnlevel. Enable tracing in VS Code or use--traceon the CLI.JSON parse failures: If your output isn’t valid JSON, the host silently returns empty diagnostics. Test your JSON serialization in unit tests.
Byte offsets: Offsets must be valid UTF-8 byte positions in the input text. Off-by-one errors here cause garbled diagnostic ranges.
Empty output: Return
[](empty JSON array), not an empty string, when there are no issues.
Other Languages¶
The Extism PDK is available for many languages:
Language |
PDK |
Build Target |
|---|---|---|
Rust |
|
|
Go |
|
|
JavaScript |
|
Javy / QuickJS |
AssemblyScript |
|
asc compiler |
Zig |
|
|
C/C++ |
|
|
See the Extism documentation and the WASM plugins reference for details.
Reference Implementation¶
The plugins/wordiness-check/ directory in the Language Check repository is a complete working example. It detects wordy phrases (e.g. “in order to” → “to”) and includes:
Detection logic with word-boundary matching
Comprehensive unit tests
TinyGo build configuration
Rust integration tests (
rust-core/tests/wasm_plugin.rs)