CLI Reference
The wax binary is the primary interface for the Wax toolchain. It supports conversion between Wax, WebAssembly Text (WAT), and WebAssembly Binary (Wasm) formats.
Usage
wax [OPTIONS] [INPUT] # convert (the default command)
wax convert [OPTIONS] [INPUT] # the same, named explicitly
wax format [OPTIONS] FILE… # reformat files
wax check [OPTIONS] FILE… # validate files
wax lsp # run the language server (over stdin/stdout)
By default wax converts between formats; this command is also available under
its explicit name, wax convert (useful when an input filename could be
mistaken for a subcommand). The format subcommand reformats files (see
Formatting), check validates them (see Checking),
and lsp starts a Language Server Protocol server for editors (see Language
server).
Positional Arguments
[INPUT]: Source file to convert/format. Supported extensions:.wax,.wat,.wasm.- If omitted,
waxreads fromstdin.
- If omitted,
Options
-
-o,--outputFILE- Output file. Writes to
stdoutif not specified.
- Output file. Writes to
-
-f,--format,--output-formatFORMAT- Specify the output format.
- Values:
wax,wat,wasm. - Default:
wax(if not auto-detected from output filename).
-
-i,--input-formatFORMAT- Specify the input format.
- Values:
wax,wat,wasm. - Default: Auto-detected from input filename, or
waxif reading from stdin.
-
-v,--validate- Force validation, and additionally report unused locals.
- For Wax: Runs type checking.
- For Wasm Text: Runs well-formedness checks.
- A text input (Wax/WAT) is validated by default before it is converted to a different format: the conversion and lowering passes trust that their input is well-formed, so a malformed module is rejected up front rather than reaching them. A same-format conversion (
wat→wat,wax→wax) only re-prints and is not validated by default; a Wasm binary input is trusted and not validated.--validateforces validation in every case. - Reports a warning for any local that is declared but never read: a Wax
letbinding, or a Wasm Text(local …). This reporting is only enabled by--validate(the default validation above does not turn it on). Prefix the name with_(e.g.let _x = …,(local $_x i32)) to mark it intentionally unused and silence the warning; function parameters are never reported. Thisunused-localwarning can be silenced, kept, or promoted to an error with-W(e.g.-W unused-local=error). - For input containing conditional annotations (
#[if]/(@if ...)), every reachable combination of conditions is checked independently, and each error is reported with the assumption (reachable when ...) under which it occurs.
-
-s,--strict-validate- Perform strict reference validation for Wasm Text input (overrides the default relaxed validation). Wasm binary input is always validated strictly, regardless of this flag. When compiling Wax or Wasm Text to a binary, functions referenced by
ref.funconly inside a function body are automatically declared (via a declarative element segment), so the output passes strict validation.
- Perform strict reference validation for Wasm Text input (overrides the default relaxed validation). Wasm binary input is always validated strictly, regardless of this flag. When compiling Wax or Wasm Text to a binary, functions referenced by
-
-DNAME[=VALUE],--defineNAME[=VALUE]- Set a conditional-compilation variable, specializing
#[if(...)](Wax) and(@if ...)(WAT) annotations. A fully determined conditional is removed (its surviving branch is spliced in) and a partially determined one is kept with its condition simplified (the set variables substituted, the rest left). Comments inside a removed branch are dropped. - The value kind is inferred: a bare NAME (or
NAME=true/NAME=false) sets a boolean;NAME=N.N.N(three integers) sets a version; any otherNAME=VALUEsets a string. - Repeatable, to set several variables. Has no effect on Wasm binary input (which carries no conditionals).
- Set a conditional-compilation variable, specializing
-
-XNAME[=on|off],--featureNAME[=on|off]- Enable (or disable) an optional WebAssembly proposal, off by default.
Bare NAME or
NAME=on/true/yesenables it;NAME=off/false/nodisables it. Repeatable. Known features:custom-descriptors: exact reference types (&!t),descriptor/describesstruct clauses, and the descriptor instructions. Without it these constructs are rejected during validation.compact-import-section: write same-module imports under one module name in the binary import section. What the flag enables depends on the input, because the text form is authoritative for import layout. From a text input (Wax or WAT), aimport "m" { … }block /(import "m" (item …) …)group is lowered to a compact entry — a name-only group sharing one type when the items’ types all match, else one type per item; a Wax block of one item flattens to a plain import, and imports the source left separate are never merged. From a WASM binary — which has no authorial text layout — the flag instead coalesces runs of consecutive same-module plain imports (the “compress this binary” mode). Groups written explicitly, or already present in a binary, are preserved through WAT↔WAT and WASM↔WASM round-trips on their own, no flag needed.
- A module can also declare the features it uses itself, with a
#![feature = "NAME"]inner attribute (WAT: a module-level(@feature "NAME")annotation); see Features. The declared and enabled sets are unioned. The one exception is a conflict: an explicit-X NAME=offagainst a module that declares NAME is an error, reported at the attribute, since the file states a fact and the flag states a policy and neither silently wins. - A binary persists the declarations as
+NAMEentries of the conventionaltarget_featurescustom section, one entry per declared feature; other producers’ entries there are preserved verbatim and not interpreted. Decompiling a binary restores the attributes from the union of those entries and the gated encodings the module actually uses.
- Enable (or disable) an optional WebAssembly proposal, off by default.
Bare NAME or
-
-WNAME=LEVEL,--warnNAME=LEVEL- Set the reporting level of a warning produced during validation.
- NAME is a single warning, a group, or
all:-
unused-local(groupunused): a local that is declared but never read. Produced while validating; shown by default. Carries a quick-fixeditthat inserts a_at the name’s start. -
unused-field(groupsunused,correctness): a module field that nothing reachable references — a function, global, memory, table, tag, type, or a passive data or element segment. An active segment (and a declarative one) runs at instantiation, so it counts as used whether or not an instruction names it; only a passive segment, reachable solely throughmemory.init/table.initanddata.drop/elem.drop, can be dead. The module-level analog ofunused-local; prefix its name with_to silence one. Shown by default.Liveness is reachability from the roots, not the mere presence of a reference, so a dead cycle is caught: two functions that only call each other reference one another, yet neither can ever run. The roots are the functions reachable from outside — exported, or the start function — and every reference made from a module-level context, which runs at instantiation: a global or table initializer, an element or data segment. A declarative element segment is the exception, since it installs nothing and runs nothing: it exists so that a
ref.funcelsewhere validates, so the function it names is reachable exactly when that otherref.funcis. Liveness then follows calls, and taking a function reference counts as calling it, since where the reference ends up is not tracked — so the analysis never reports a function that might run. A field that only dead code references is dead in turn: a global read solely from an unreachable function is reported. One consequence worth knowing: in a module that exports nothing and has no start function, nothing can run, so every field is reported.Types take part in the same analysis, with one extra kind of edge: a type definition’s own components reference types (its supertype, a field or element type, a
descriptorclause), and those only count if the definition itself is live. So arecgroup nothing outside it names is dead as a whole, however much its members name each other, and a type named only by an unreachable function’s signature or body is dead too. Only definitions written in the source are candidates: the implicit function types interned for an inline block orcall_indirectsignature are not reported.A type is used wherever the source names it, including the positions whose reference the compiled module carries implicitly: a table’s element type, an imported function’s signature, and the array type a string literal builds (a bare string names no type, so a like-typed definition it is compiled onto counts as used). The converse also holds, and is why a Wax module can report a type its compiled form references:
type t = fn(); #[export] const g = f;namestnowhere, so it is reported, even though the global’s lowered type(ref $t)points at the definition (a reference type can only name a type). Deleting it recompiles: the lowering then interns an anonymous function type in its place. -
unused-import(groupsunused,correctness): an imported function, global, memory, table, or tag that nothing reachable references. Likeunused-field— same reachability analysis — but for imports;_silences one. Shown by default. -
unused-label(groupsunused,correctness): a block label that is declared but never branched to. Prefix its name with_to silence one. Shown by default. On the Wax side it carries a quick-fixeditthat deletes the whole'name:prefix. -
unnecessary-mut(groupredundant): a global declared mutable but never assigned, so it could be immutable — Waxconstinstead oflet, Wasm withoutmut. Only a module-defined, non-exported global is flagged: an import’s mutability is part of the linking contract, and an exported mutable global may be assigned by the host. A_prefix silences one, and a global that is not used at all is reported asunused-fieldinstead, so a declaration never draws both. Shown by default (unlike the rest of theredundantgroup). -
shift-count-overflow(groupcorrectness): a shift by a constant count at least the operand’s bit width. Wasm masks the count modulo the width, so the shift is almost certainly not what was meant. Shown by default. -
constant-trap(groupcorrectness): an operation that always traps on a constant operand: an integer division or remainder by zero, or a trapping float-to-integer conversion of an out-of-range constant. Shown by default. -
tautological-comparison(groupcorrectness): a comparison whose result is constant regardless of its variable operand: an unsigned comparison against zero (x >=u 0,x <u 0), or two identical operands (x == x). Shown by default. -
constant-condition(groupcorrectness): a branch, loop, orselectcondition that is a constant literal (the idiomatic infinite loopwhile <nonzero>is not flagged). Shown by default. -
unused-result(groupcorrectness): the result of a side-effect-free expression is computed and then discarded, as in_ = x + 1. Covers reads, constants, pure arithmetic, and heap allocations (a discarded struct/array literal) whose operands are themselves effect-free. Shown by default. -
dead-code(groupcorrectness): a statement that can never be reached, following an unconditional branch,return, orunreachable. Shown by default. -
cast-always-fails(groupcorrectness): a reference cast or test whose operand can never have the target type (the two are unrelated in the type hierarchy), so the cast always traps and the test is always false. Shown by default. In a chain only the innermost such cast is reported: an outer cast or test over a value that can never be produced says nothing the inner verdict does not already, and the fix belongs at the inner cast. -
eager-select(groupcorrectness): a trapping or effectful operation in a branch of a?:(which compiles to aselect, evaluating both branches unconditionally), so it runs even when the condition selects the other branch. Shown by default. On Wasm input the check covers a foldedselect(its value operands are distinct subtrees); an unfoldedselectis not flagged. -
precedence(groupcorrectness): two operators whose relative precedence is easy to misremember are mixed without parentheses: a shift (<</>>) with arithmetic (+,-, …), or a comparison with a bitwise operator (&,|,^). The code is correct, but a reader may misread the grouping (1 << nbits - 1is1 << (nbits - 1)). Shown by default. Wax-only: WAT/Wasm have no infix precedence. Carries a quick-fixeditthat wraps the tighter-binding sub-expression in parentheses. -
redundant-operation(groupredundant): an operation with no effect on its result: an arithmetic identity (x + 0,x * 1,x << 0, …), an absorbing operand (x * 0,x & 0), two identical operands (x - x,x ^ x), a self-assignment (x = x), or a cast to a type the operand already has. Hidden by default (these are common in generated code); enable with-W redundant=warning. -
truncated-coverage: path-sensitive validation gave up after too many conditional configurations. Shown by default. -
confusable-unicode(groupcorrectness): a string the module carries — an export/import name, a string literal, a data segment, or a conditional string — contains a “Trojan Source” bidirectional control character (U+202A/B/D/E, U+2066–2069, U+206C) that can make the displayed source read differently than it runs. Shown by default, reported on Wax, WAT and Wasm input alike. -
naming-conflict(groupnaming): converting from Wasm, a source name collided with another and was renamed (e.g.foo→foo_2). Hidden by default. -
reserved-word-rename(groupnaming): converting from Wasm, a source name is a Wax reserved word and was renamed (e.g.if→if_2). Hidden by default. -
generated-name(groupnaming): converting from Wasm, an unnamed but referenced parameter was given a generated name (e.g.x), since it cannot be rendered anonymously. Hidden by default. -
compound-assignment(groupsuggestion): a plain assignmentx = x op ecan be written with the compound operatorx op= e(arithmetic and bitwise operators only;xmust be the operator’s first operand, sox = e - xis left alone). Hidden by default. -
field-punning(groupsuggestion): a struct field initialised from the like-named local or global,{x: x}, can use the punning shorthand{x}. Hidden by default. -
redundant-annotation(groupsuggestion): a type the inferred type already makes redundant and so can be dropped: aletannotation (let x: t = e→let x = e, including the anonymous_: t = eand each binding of a tuplelet (a: t, b) = e), a module-level global annotation (let counter: i32 = 0→let counter = 0, likewise for aconst), a construction type name ({T| …}→{…}), a block result type (do t { … }→do { … }), or anifresult type (if c => t { … }→if c { … }). Hidden by default.
-
- The
suggestiongroup is reported at a distinct Suggestion severity (not a warning), and each entry carries a machine-applicable rewrite. These are optional simplifications, so they are hidden by default and surface as on-demand quick fixes in the VS Code extension;wax checkprints them when enabled with-W suggestion=warning(or an individual name). A redundant cast removal is offered too, but as a fix attached to the existingredundant-operationwarning rather than a separate suggestion. - LEVEL is one of:
hidden: suppress the warning entirely.warning: report it as a warning.error: promote it to an error, so the run fails.
- Repeatable; later settings override earlier ones. For example,
-W all=error -W unused-local=warningmakes every warning fatal except unused locals. - The validation warnings (
truncated-coverage, and theunusedandcorrectnesslints) are produced only while validating.truncated-coveragecan arise from the validationconvertruns on a text input; theunused/correctnesslints are turned on only by--validate, and always forcheck. Thenamingwarnings are produced when converting a Wasm module to Wax. - Almost all of these lints apply to WebAssembly text/binary input as well
as Wax: the Wasm validator runs the same checks, so
wax check foo.watreports them. (The exceptions areprecedence, which is Wax-only since WAT/Wasm have no infix precedence, andeager-select, whose Wasm side covers only a foldedselect.) On Wasm inputunused-resultcovers discarding a constant or a purelocal.get/global.getread; forunused-labela numericbr Ncounts as a use of the labelNlevels out;cast-always-failsandredundant-operationreason aboutref.cast/ref.testand constant operands directly from the bytecode. As withunused-local, they fire only when unused reporting is on (undercheck, or under a conversion with--validate). - The
WAX_WARNenvironment variable sets default levels applied before the-Woptions. Its value is a list ofNAME=LEVELspecs separated by commas or whitespace, e.g.WAX_WARN="correctness=hidden unused-local=error". Unlike a plain environment fallback, these defaults still apply when-Wis given: the command line only refines them (soWAX_WARN=correctness=hiddenwith-W dead-code=warninghides the tier except dead code). A malformed or unknown entry is reported on stderr and skipped.
-
--colorWHEN- Colorize output.
- Values:
always,never,auto. - Default:
auto(colors enabled only if output is a TTY).
-
--error-formatFORMAT- How diagnostics are rendered.
- Values:
human(source snippets, the default),json, orshort. - With
human, a named warning’s-Wname is appended to the header as[name](e.g.Warning [unused-local]:), so at a glance you can tell which warning fired and which-Wname silences or promotes it. - With
json, every diagnostic — errors, warnings, and syntax errors — is written to stderr as one JSON object per line (JSON Lines), for an editor, CI job, or AI assistant to parse. Each object hasseverity,file,startLine/startColumn/endLine/endColumn(1-based line, 0-based column),startOffset/endOffset(byte offsets),message,warning(the-Wname, or null),hint, andrelated. A diagnostic carrying a machine-applicable fix (asuggestion, a fixable warning like a redundant cast, an unused local, an unused label, or a confusing precedence mix, or a syntax error whose recovery insertion derives one — e.g. a missing;, under--all-errors) also has aneditobject: the same span fields for the slice to replace (an empty span, for an insertion), plus itsnewText. Syntax errors report theirhintandrelatedhere too. - With
short, each diagnostic is onefile:line:col: severity: messageline (gcc/rustc style, 1-based column), for an editor with a line-based error parser (Vim’serrorformat, Emacs Flymake, …). A named warning’s-Wname is appended as[name]. - Exit codes are unchanged. Also accepted by
checkandformat.
-
--fold- Fold instructions into nested S-expressions.
- Affects WAT output (the default form is chosen automatically).
-
--unfold- Unfold instructions into flat instruction lists.
- Affects WAT output (the default form is chosen automatically).
-
--desugar- Expand the Wax-specific
(@string …)and(@char …)annotations into core WebAssembly (array.new_fixed/i32.const), so the output is plain WebAssembly text that other tools accept. Ani8string keeps its raw UTF-8 bytes; ani16string is UTF-16-encoded; an untyped string reuses an existing(array (mut i8))type when the module has one and otherwise pins a synthesised one. A module-level(@string …)global becomes an ordinary global. - Synthesises the declarative element segment (
(elem declare func …)) that Wax’s lenient reader lets a module omit for a function used byref.funconly inside a body, so the output passes strict/spec reference validation. This heals a conditional module: WAT emitted from a#[if]module keeps its(@if …)and omits the segment, but once the conditionals are resolved with-Dand desugared the segment is added. It is a no-op when the segment is already present (as on the wax-to-wat and binary-to-wat paths). - Only valid with wat output (
-f wat); requesting it for wasm or wax output is a usage error. - Fails (exit
128) if a conditional-compilation directive(@if …)remains unresolved; resolve them first with-D/--define. - Only Wax-specific annotations go. The
metadata.code.*hints of the branch-hinting and compilation-hints proposals are real WebAssembly text, so they stay. - A
(@feature "…")declaration is stripped, so none remains in the output. Feature resolution has already run by then, so the declaration still gates during processing; re-ingesting the desugared text needs-Xagain, exactly as desugared strings do not come back as literals. A module using gated constructs is not an error — those are real (proposal) WebAssembly, and removing annotations is orthogonal to whether the consumer supports the proposal.
- Expand the Wax-specific
-
--faithful- Decompile faithfully: turn off the stream-reshaping recoveries, so a
wat/wasmmodule decompiled to Wax recompiles with the same reachable instruction structure. It keeps!(a == b)instead of fusingt.eq; i32.eqzinto a singlea != b, and keeps a flatbr_on_cast_failchain instead of recovering it as amatch(which re-lowers to the nested ladder). It also keeps a redundant non-nullref.cast. - Only valid with wax output (
-f wax); requesting it for wat or wasm output is a usage error. - Width fidelity is not faithful-only: constant, leftover, dead-code and
convert /
extend32_swidths are pinned on the default decompile too, so--faithfulpreserves the opcode structure the reshaping recoveries would change, not the widths. - Some divergences remain (all semantically inert): local ordering and
numbering, the
namesection, type renumbering, compiler-inserted casts, a typedselect’s immediate, and the shared spellings (extend32_s, a 32-bit load widened toi64, the call family, float neg-of-literal). See Round-Tripping.
- Decompile faithfully: turn off the stream-reshaping recoveries, so a
-
--source-map- Generate a source map file alongside the output file and insert a
sourceMappingURLcustom section. Only valid with wasm output (-f wasm) to a file, when the source is a text file (not a Wasm binary). Requesting one for wat or wax output, when the source is a Wasm binary, or when outputting tostdout, is an error.
- Generate a source map file alongside the output file and insert a
-
--debugCATEGORY- Enable developer debug output for a category. Repeatable, and a single
value may list several categories separated by commas
(
--debug timing,…). - Categories:
timing: log the wall-clock running time of each compiler pass (parse, specialize, validate, type-check, convert, output) to stderr, one line per pass as it finishes. The normal output on stdout is unchanged.width-check: on a wasm or wat to wax conversion, report a width disagreement instead of repairing it. The conversion records the type of each value the source opcodes produce, and the type checker — which already runs over the decompiled module — reconciles its own inference with those records: an expression that would recompile at another width (ani64operation re-read asi32, say) is PINNED with a cast at the width the WebAssembly states. That is how the decompiler keeps widths faithful, so a pin appearing in the output is the mechanism working, not a fault. This category reports each such pin as an error instead, which is what a tool developer needs to see WHERE the decompiler is relying on it and why — it is a self-check on the decompiler, not a check on the input, and it does nothing for any other conversion. A disagreement a pin cannot fix — the value’s type is fixed by its context, so a cast would convert the value instead of grounding it — is an error either way; that means the WebAssembly is invalid (a binary input is trusted, never validated — check it withwax check) or the decompiler is wrong.
- Enable developer debug output for a category. Repeatable, and a single
value may list several categories separated by commas
(
-
--version- Print the toolchain version and exit. In a released build this is the git
tag it was built from; a plain development build reports
dev.
- Print the toolchain version and exit. In a released build this is the git
tag it was built from; a plain development build reports
-
--help- Show the manual page and exit. Also available per subcommand
(
wax format --help, etc.).
- Show the manual page and exit. Also available per subcommand
(
Examples
Convert a Wax file to Wasm binary:
wax input.wax -o output.wasm
Convert a Wasm Text file to Wax (decompilation):
wax input.wat -o output.wax
Format a Wax file (round-trip):
wax input.wax
Read from stdin and write to stdout:
cat input.wax | wax -f wasm > output.wasm
Specialize conditional compilation while converting:
wax input.wax -D debug=false -D ocaml_version=5.1.0 -D target=wasi -o output.wasm
Formatting
The format subcommand reformats files in their own format (.wat → .wat,
.wax → .wax, .wasm → .wasm), detected from each file’s extension.
Formatting never validates: it only re-prints; use check to
validate.
With no FILE, format reads standard input and writes the formatted result to
standard output. The format cannot be detected without a filename, so --format
is required in this mode (and --inplace / --check, which act on named files,
are not allowed). This is the interface an editor formatter or a shell pipe
uses:
echo 'fn f(x:i32)->i32{x*x;}' | wax format -f wax
wax format [OPTIONS] FILE…
Options
-i,--inplace- Write the formatted output back to each input file.
- Without this flag (and without
--check), exactly one file must be given and its formatted output is written tostdout— or no file, to formatstdintostdout(see above).
-c,--check- Write nothing; list the files that are not already formatted and exit with
a non-zero status if any are found. Useful in CI. Cannot be combined with
--inplace.
- Write nothing; list the files that are not already formatted and exit with
a non-zero status if any are found. Useful in CI. Cannot be combined with
-f,--format,--input-formatFORMAT- Treat all input files as this format (
wat,wasmorwax), overriding the detection from each file’s extension.
- Treat all input files as this format (
-WNAME=LEVEL,--warnNAME=LEVEL: set a warning’s level, as above.--colorWHEN: as above (ignored when writing back in place).--error-formatFORMAT:human,json, orshort, as above (a malformed file still reports its syntax error).--fold/--unfold: as above.--debugCATEGORY: as above.
Examples
Format a single file to stdout:
wax format input.wat
Reformat several files in place:
wax format -i a.wax b.wax c.wax
Check formatting in CI (non-zero exit if any file differs):
wax format --check src/*.wax
Checking
The check subcommand validates files (type-checking for Wax, well-formedness
for Wasm) without producing any output, reporting diagnostics and exiting with a
non-zero status if any file fails. It takes one or more files. Unused-local
warnings (see --validate) are reported too, but do not by
themselves cause a non-zero exit, unless promoted to an error with
-W (e.g. wax check -W unused-local=error src/*.wax).
wax check [OPTIONS] FILE…
Options
-f,--format,--input-formatFORMAT: force the format of all files, overriding extension detection.-s,--strict-validate: strict reference validation for Wasm Text, as above (Wasm binary is always strict).-DNAME[=VALUE],--defineNAME[=VALUE]: set a conditional-compilation variable, as forconvert: the conditionals are specialized before validation. A full set validates one configuration; a partial set leaves the remaining conditionals for the path-sensitive check to explore. Repeatable.-XNAME[=on|off],--featureNAME[=on|off]: enable or disable an optional proposal, as forconvert. Repeatable.-WNAME=LEVEL,--warnNAME=LEVEL: set a warning’s level, as above.--all-errors: report every syntax error instead of stopping at the first. Normally the parser gives up at the first unexpected token; with this flag it uses panic-mode error recovery and continues, so a single run lists all the syntax errors. Wax resynchronizes at statement and block boundaries (;,},),], and the keywords that begin a new item or statement); WAT, being fully parenthesized, resynchronizes on the parentheses (dropping an incomplete group, auto-closing an unclosed one). The recovered module is then checked too — type-checked for Wax, validated for WAT — so genuine type or validation errors in the intact regions surface alongside the syntax errors, while the cascades a dropped construct would otherwise trigger (an unbound name in Wax; a warning or wrong stack height in WAT) are suppressed. Text input only, Wax and Wat (ignored for a Wasm binary).--colorWHEN: as above.--error-formatFORMAT:human,json, orshort, as above.--debugCATEGORY: as above.
Example
Type-check several Wax files (e.g. in CI):
wax check src/*.wax
Report all syntax errors in a Wax file at once:
wax check --all-errors src/foo.wax
Language server
The lsp subcommand starts a Language Server
Protocol server for Wax
and WebAssembly text, speaking JSON-RPC over stdin/stdout. It runs the same
analysis the VS Code extension runs in-process, exposed to any LSP-capable
editor (Neovim, Emacs with Eglot or lsp-mode, Helix, Kakoune, Zed, and others).
wax lsp
It reads no files of its own; the editor tells it which documents are open.
Supported requests: diagnostics (pushed on open/change), hover,
go-to-definition, go-to-type-definition, find-references, document highlight,
rename (with prepare), document symbols (outline), completion, signature help,
inlay hints, folding ranges, selection ranges, semantic tokens, and document
formatting. The hover, navigation and completion features that need the typed
tree are Wax-only, while formatting, diagnostics and the outline work for .wat
too (dispatched by the document’s URI extension).
A lint diagnostic carries the extra metadata editors use: the -W name as its
code (linked to this reference), and DiagnosticTag.Unnecessary on the
removable/unreachable lints (unused bindings, dead code) so the editor fades
them.
The one setting is wax.define, a list of conditional-compilation defines
(mirroring the -D flag, e.g. ["debug=true", "arch=wasm64"]).
Diagnostics and completion specialize to it, so a definition or an error in a
branch the defines rule out is dropped. The server reads it from the client’s
initializationOptions at startup and from workspace/didChangeConfiguration
live (under a wax section), re-checking the open documents when it changes.
Documents are synchronized in full (each change carries the whole buffer). The
position encoding is negotiated: the server uses UTF-8 when the client offers it
and otherwise UTF-16, the LSP default that every client supports. The --stdio
flag is accepted (and ignored) for the editors that pass it by convention.
Editor setup
Most editors just need the launch command wax lsp bound to the wax (and, if
desired, wat) file type through their LSP client. Ready-to-use configurations
for Neovim, Helix, and Emacs — paired with the tree-sitter-wax grammar for
highlighting — live under editors/
in the repository (one directory per editor, each with a README).
Exit status
All three commands share the same exit codes:
| Code | Meaning |
|---|---|
0 | Success. For check / format --check, also means every file passed. |
123 | A usage error: an invalid combination of flags (e.g. --source-map with text output or binary input, or binary output to a terminal), or a format --check run that found files needing formatting. |
124 | A command-line parse error: an unknown flag or a bad option value (reported by the argument parser). |
125 | An internal error (an uncaught exception). |
128 | The input was rejected by a diagnostic: a parse, validation, or type error, or a malformed wasm binary. This is also the status of a check run that found any problem. |
The distinction is how you used the tool (123/124) versus the input is
bad (128). A rejected input reports 128 wherever the error is found
(syntax, validation, or type-checking) so check exits 0 when every input is
valid and 128 otherwise, which is what a CI gate wants. For scripting, treat
any non-zero status as failure.
Shell completion
wax supports zsh, bash, and PowerShell completion: subcommands, flag
names, flag values (--format → wat/wasm/wax, -W/-X names and values,
--color, --debug), and input/output file paths.
Completion has two halves. The generic per-shell scripts ship with the
cmdliner package; the wax
package installs its own completion definitions (and man pages) alongside the
binary. Installing wax through opam puts both in place. (These extra files are
installed on Unix only; they are skipped on native Windows.)
Then hook the generic scripts into your shell once. For zsh, ensure the
site-functions directory is on $fpath before compinit:
FPATH="$(opam var share)/zsh/site-functions:$FPATH"
autoload -Uz compinit && compinit
For bash, with bash-completion
installed, point it at the cmdliner share directory. See the
cmdliner completion guide
for the per-shell details and the PowerShell setup.