Skip to content

The dusk CLI

The dusk binary drives the compiler. It has fourteen commands. Eight stop the pipeline at a stage and dump that stage’s output, for inspecting a single pass: lex, scan, parse, load, desugar, check, mono, and esc. Three compile: build to a native binary or, with --lib, to a static archive a C host links, ir to LLVM IR text on stdout, and run to build and execute. doc renders a module’s doc comments as markdown or, with --json, as a machine model, demo is a toolchain smoke test, and version prints the version. Two commands carry a flag that changes what they emit: check --json reports diagnostics as a JSON document, and ir --target=wasm32 cross-emits for wasm32-wasip1. Running dusk with no command prints dusk: no command given followed by the usage text and exits with a failure status; anything the binary does not recognize prints dusk: unknown command above the same text and exits the same way.

usage: dusk <command> [file]
commands:
version print the compiler version
demo emit the spine IR, link it with clang, and run it
lex <file> dump the token stream of a source file
scan <file> dump the paradigm and import directives of a file
parse <file> lex + parse, dump the AST
load <file> load imports, dump the merged AST
desugar <file> load + desugar, dump the merged AST
check <file> type check a source file and report diagnostics
check --json <file> report diagnostics as JSON
mono <file> check, then dump the mono-expanded ground module
ir <file> lower the mono-expanded module to LLVM IR
ir --target=wasm32 <file> cross-emit the LLVM IR for wasm32-wasip1
build <file> lower, link, and write target/dusk-out
build --lib <file> archive the module and its runtime into libstem.a + stem.h
run <file> build and run, forwarding trailing args
esc <file> dump native escape summaries
doc <file> render a module's doc comments as markdown
doc --json <file> emit the module's doc model as JSON

There is no help command and no flag spelling of one. dusk help, dusk -h, and dusk --help are all unknown commands, so they print the usage text by way of the unknown command path rather than as a request you made; the usage text is the same either way, which is why reaching for one still tells you what you wanted to know.

To install the toolchain in the first place, see getting started. The package tool dawn has its own page at Dawn.

The inspection commands expose the pipeline one stage at a time. Each takes a single source file, prints that stage’s result to stdout, reports diagnostics on stderr, and exits with a failure status when there are errors.

dusk lex <file> dumps the token stream. Each line shows the token’s byte span in the source followed by the token kind.

dusk scan <file> runs the pre scan, which reads only the file’s directives, and prints the effective paradigm set and the import list.

Terminal window
dusk scan examples/app.dusk
paradigms: [Procedural]
imports:
std.io
std.string
std.functional.maybe
std.functional.either
std.memory.arena

See source files for the @paradigm and @import directives themselves.

dusk parse <file> lexes and parses the file and dumps the resulting AST in the compiler’s debug format, then reports any lex or parse errors.

Four more commands stop at a later stage and dump it. dusk load <file> resolves the file’s imports and dumps the merged AST; dusk desugar <file> runs the desugaring pass on top of that and dumps the result; dusk mono <file> type checks and then dumps the monomorphized AST, every generic instantiated; and dusk esc <file> dumps the escape summary oracle, the per function record of what each parameter escapes into that drives the interprocedural escape check (see memory management). All four are inspection aids, and none writes an artifact.

dusk check <file> runs the whole front end: lex, parse, name resolution, and type checking, including the file’s imports. It produces no artifacts. On success it prints ok: followed by the path; otherwise it prints the diagnostics and exits with a failure status. Each diagnostic renders three lines: a header naming the error, the source line it points at, and a run of carets under the offending span, with the caret columns counted in Unicode scalar values so they line up under multibyte characters.

Terminal window
dusk check examples/app.dusk
ok: examples/app.dusk

dusk check --json <file>, added in 1.7.1, runs the exact pipeline check runs and reports the result as data rather than as text a person reads. stdout is exactly one JSON document followed by one trailing newline in every outcome, clean or broken, stderr stays silent, and the exit code mirrors the human command, a read error on the root file included, reported inside the envelope rather than beside it. The diagnostics appear in the order the human command prints them, so the two faces never disagree about what went wrong first.

Each diagnostic carries its message, its severity, and a span. A span gives file local byte offsets, lo and hi clamped to UTF-8 boundaries, alongside 1 based line and column pairs counted in Unicode scalars at both ends, so a consumer can work in bytes or in editor coordinates without recomputing either. A diagnostic in an imported file names that file and carries offsets local to it, and a diagnostic with no position, an unresolvable import for one, carries a null span. severity is always "error" today; the key exists so a warning can arrive later without breaking the schema, and a consumer is expected to ignore keys it does not know. This is the contract the language server consumes to place a diagnostic at the precise span the compiler flagged.

dusk build <file> runs the front end and then compiles the module to a native binary. It succeeds quietly: on a clean build it prints nothing at all and exits zero, leaving the artifacts on disk for you to find. Anything it does print is a diagnostic.

Terminal window
dusk build examples/app.dusk

Code generation goes through textual LLVM IR and links against the toolchain’s small C runtime, so clang and LLVM 22.x must be on your path. The textual IR targets one LLVM major version.

Artifacts land in target/dusk-out, relative to the directory you run the command from. For a source file app.dusk you get two files named after the source file’s stem:

  • target/dusk-out/app.ll, the emitted textual LLVM IR
  • target/dusk-out/app, the linked native binary

dusk run, dusk ir, and dusk demo use the same directory.

dusk build --lib <file> compiles the module into a linkable C library instead of an executable, which is the one place the build command changes shape. It landed in 1.4.3. Two artifacts go to the same target/dusk-out directory, and unlike a plain build this one names them as it writes them:

Terminal window
dusk build --lib mylib.dusk
[dusk] archive : target/dusk-out/libmylib.a
[dusk] header : target/dusk-out/mylib.h

For a source file whose stem is mylib you get lib<stem>.a, a static archive bundling the module’s object together with every dusk runtime object so a host needs nothing else of dusk’s, and <stem>.h, a generated C header carrying one prototype per export "C" function inside an extern "C" guard. The header’s C types come from the same lowering that emitted the symbols, so it cannot drift from the archive.

The module may omit main entirely. Monomorphization roots every export "C" function, so the exports and everything they reach compile with nothing calling them from dusk’s side. A host then links the two together, as in clang host.c -I target/dusk-out -L target/dusk-out -lmylib -pthread -lm.

The archive is static only in this release. Its objects are not position independent and the runtime’s thread local storage takes the local exec model, so the archive does not link into a shared object that a dlopen based FFI such as Python’s ctypes loads at run time. See C libraries for the export rules, the header, and the whole story of the boundary in this direction.

dusk ir <file> runs the same front end and code generation as build, but instead of writing a .ll file and invoking clang, it prints the generated textual LLVM IR straight to stdout. Nothing links and no binary is produced, so it is the fastest way to read exactly what the compiler emits for a program, the same IR the playground shows beside your source.

Terminal window
dusk ir hello.dusk

Because ir skips the clang step, it runs without a native toolchain on the path. It is also the check the bootstrap leans on: two compilers agreeing on IR text byte for byte is a sharper test than agreeing on a linked binary, so the self-hosting differential compares ir output, not just built binaries.

dusk ir --target=wasm32 <file>, added in 1.6.0, cross-emits the module for wasm32-unknown-wasip1 instead of the native triple. The one difference in the IR is the entry point: wasi-libc’s crt1 calls __main_argc_argv where a native C runtime calls main, so a main declared with argc and argv takes that entry symbol, and a nullary main gets a wrapper that accepts and drops the two wasi arguments. Everything else the compiler emits is unchanged, and dusk ir with no flag emits the native triple exactly as it always has.

Terminal window
dusk ir --target=wasm32 compiler/dusk.dusk > dusk.ll

This is the form a wasi toolchain links and the shape the browser playground is built from: the self-hosted compiler cross-emitted to wasm32 and linked with a wasi sysroot runs the whole front end in the browser. The link that consumes the IR happens outside the compiler, with clang --target=wasm32-wasip1, a wasi sysroot, and the runtime’s wasm_shim.c standing in for the process and shell layer wasi has no shape for, so the toolchain list a native build needs is unchanged. Note that this flag sets the triple of the module the compiler is compiled into, not the code a compiled program emits: a dusk.wasm built this way still emits native IR for the user programs it compiles.

dusk run <file> compiles the file exactly like build and then executes the binary. Any arguments after the file are forwarded to the program, so a main declared with argc and argv sees them. The exit status of dusk run is the program’s own exit code.

echo.dusk
@paradigm procedural
// main may take argc and the argv string slice; dusk run forwards
// trailing arguments here.
func main(argc: int32, argv: string[]) -> int32 {
for arg in argv {
println(arg)
}
return 0
}
Terminal window
dusk run echo.dusk hello world
target/dusk-out/echo
hello
world

As in C, argv element zero is the program path and the forwarded arguments follow, so argv.len matches argc.

dusk demo builds and runs the Phase 0 spine: a hardcoded IR program that is linked and executed without any dusk source involved. It prints the paths of the emitted IR and the linked binary, runs the program, and reports its exit code. Use it as a smoke test that clang, LLVM, and the C runtime are wired up correctly.

dusk doc <file>, added in 1.6.1, renders a module’s doc comments as markdown. It reads the single file with no import loading and no type check, and it emits every item in source order with its signature rebuilt from the declaration itself and its prose reproduced verbatim, so the documentation cannot drift from the code it describes. An undocumented item still appears, listed with its signature alone. An emitted code fence grows one backtick past the longest backtick run inside its content, so an @example that carries a fence of its own renders intact.

Terminal window
dusk doc lib/std/vector.dusk

dusk doc --json <file> emits the same model as JSON with a fixed key order, two space indentation, and null for an absent doc, the contract a tool consumes. Since 1.7.1 every item object also carries a span key with its name token’s location, so a consumer can jump from a doc entry to the declaration it came from. Both faces refuse rather than emit a lie: a doc comment that binds to nothing, an unknown tag outside an example, an @param naming no parameter or repeating one, an @return on a void function, and each is located at its real byte inside the doc block, reported all at once, with stdout left empty so a doc pipeline never consumes half a page. The language server reads doc comments directly for its hover rather than shelling out to this command, but the JSON model is the same shape a tool would build on.

dusk version prints the toolchain version, for example dusk 1.11.0. That is the only spelling: --version and -V are not flags the binary knows, and each prints the unknown command usage text and fails, so reach for the bare version word. The version string is the one user visible signal of which release a binary carries. Through the self-hosting transition the compiler written in Dusk and the Rust seed both answered with the same string, so you could confirm the two agreed on their release; the repo is now pure Dusk, with the old Rust compiler archived at dusk-rust.

The compiler ships two asset directories beside itself: lib, which holds the standard library written in Dusk, and runtime, which holds the C runtime sources. The canonical compiler, compiler/dusk.dusk, resolves each by trying five locations in order; the first one that holds the asset on disk wins.

  1. DUSK_HOME. If the DUSK_HOME environment variable is set, the compiler looks for the asset under it, as in $DUSK_HOME/lib. Set this to point a binary at a different toolchain tree, such as a source checkout.
  2. The directory the binary sits in. The compiler looks for the asset right beside the running executable.
  3. The share directory one level up. An installed toolchain finds its assets one level above the directory that holds the binary, under share/dusk-lang, as in /usr/share/dusk-lang beside /usr/bin/dusk.
  4. The directory argv[0] names. The compiler also looks beside the path it was invoked as.
  5. The working directory. As a source-checkout fallback, the compiler looks under the directory you run it from, which keeps a checkout working with no environment at all.

A compiler installed at prefix/bin beside prefix/share/dusk-lang finds its assets with no DUSK_HOME set, and a packaged install works the same way. Point DUSK_HOME at a checkout root to override the search and run a compiler against a different toolchain tree.