Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Installing Praxis

Praxis is built from source. There are no binary releases and no installer: you need a Rust toolchain, and at the end of it you have a praxis binary that JIT-compiles .px files. cargo will fetch and build it for you, or you can build it from a checkout.

What you need

A stable Rust toolchain. The repository pins one in rust-toolchain.toml:

[toolchain]
channel = "stable"
components = ["rustfmt", "clippy"]

rustup reads that file when you run any cargo command inside the checkout, so it selects — and if necessary installs — the right toolchain for you. The workspace declares rust-version = "1.88" as its minimum — the floor its own dependencies impose — and anything newer on the stable channel is fine.

Nothing else is required to build the compiler. Praxis links Cranelift for code generation, so there is no LLVM to install.

Installing from crates.io

The compiler is published as fifteen crates and the binary lives in praxis-cli, which depends on the other fourteen:

$ cargo install praxis-cli

That builds in release mode and puts praxis in ~/.cargo/bin — worth having on your PATH, because the VS Code extension looks for it there by default. The crate is praxis-cli and the binary is praxis; the bare name praxis on crates.io belongs to an unrelated project.

Everything below is for building from a checkout instead, which is what you want if you are working on the compiler itself.

Building the compiler

The praxis binary is the praxis-cli crate. From the repository root:

$ cargo build --release -p praxis-cli

That produces target/release/praxis. Confirm it works:

$ ./target/release/praxis --version
praxis 0.1.0

cargo build -p praxis-cli without --release writes target/debug/praxis instead. The debug binary is a complete compiler and builds a good deal faster, which matters while you are working on the compiler itself. Use the release binary for anything you care about the running time of: the code generator and the GC heap are ordinary Rust, and an unoptimized build of them is unoptimized.

To put the binary you just built on your PATH, install from the checkout rather than from the registry:

$ cargo install --path crates/praxis-cli

That builds in release mode and copies the binary into ~/.cargo/bin, overwriting whatever cargo install praxis-cli put there.

The just runner

The repository’s quality gate is a just file. You do not need just to build or use the compiler, only to run the checks the way CI runs them.

$ cargo install just

The recipes are small, and just with no arguments lists them:

recipewhat it does
just buildcargo build --workspace
just testcargo test --workspace
just clippycargo clippy --workspace --all-targets -- -D warnings
just fmtcargo fmtmodifies files
just fmt-checkcargo fmt --check
just bookrenders the book to docs/book/book
just book-verifyre-runs every example in the book against this compiler
just cifmt-check, clippy, test, then the book’s examples
just asanthe whole suite under AddressSanitizer, on a nightly toolchain
just publish-planthe crates.io publish order, and what is already up
just publish-drypackages every crate and builds it from its own tarball

just ci is the gate, and the point of it is that it is the only gate: the hosted CI job checks out the tree, installs the toolchain, installs just, and runs just ci. It has no logic of its own, so what CI does and what you do before pushing cannot drift apart. fmt is deliberately not a dependency of ci — CI verifies formatting, it never rewrites your files. Neither is book-bless, which rewrites the book’s expectations from whatever the compiler prints today and would paper over the regression book-verify exists to catch.

Two things about just ci are worth knowing before you run it the first time.

It takes about seventeen minutes on a development laptop, and most of that is not compilation. On macOS the bulk is XProtect scanning each freshly linked test binary the first time it is executed.

Doctests are not part of it. Every library crate sets doctest = false. An example in a /// comment is still compiled by cargo doc, but it is never executed, so an assertion written there checks nothing — put it in a unit test. The reason is cost, not principle: rustdoc --test has to analyze a whole crate before it can discover that the crate has no doctests, that work is never cached, and it was costing 95 seconds a run to execute the single doctest the workspace had.

just asan needs a nightly toolchain (rustup toolchain install nightly) and is not part of ci, because an instrumented build is a second full compile of the workspace; hosted CI runs it on a nightly schedule instead. It does not cover JIT-generated code — Cranelift emits that raw and no -Z flag reaches it — so a green ASan run is necessary and not sufficient for a change that puts new unsafe behaviour in generated code.

The VS Code extension

The extension lives in editors/vscode. It is thin on purpose: it registers the .px extension, launches praxis lsp, contributes three commands and a TextMate grammar for highlighting before the server attaches. There is no parsing and no type logic in it. Everything the editor knows about your program arrives from the compiler over the protocol, so the two cannot disagree about what a file means.

Build it, then package it:

$ cd editors/vscode
$ npm install
$ npm run compile
$ npx @vscode/vsce package

That writes praxis-0.1.0.vsix in the same directory. Install it from the Extensions view’s menu → Install from VSIX…, or from a shell:

$ code --install-extension editors/vscode/praxis-0.1.0.vsix

The .vsix is gitignored, and reinstalling the same version replaces the previous one.

Then point the extension at your binary. The setting is praxis.binaryPath; it defaults to the bare name praxis, resolved on PATH. If you did not run cargo install, set it to an absolute path to target/release/praxis. That one path is used for the language server and for the run and check commands, so there is a single thing to get right; changing it restarts the server.

If the server cannot start, you get an error message naming the command it tried and the setting to change, rather than a stack trace. It is nearly always a praxis.binaryPath that points at nothing.

The three commands are Praxis: Run File, Praxis: Check File and Praxis: Restart Language Server. The first two save the buffer and then run the binary in an integrated terminal — a terminal rather than an output channel, because the crash debugger is interactive and a write-only pane cannot answer a prompt. Run File appends --input input.txt when a file by that name sits beside the source.

Everything else arrives without the extension contributing anything, because it is a server capability: diagnostics, hover, completion, signature help, go-to-definition, document symbols, semantic tokens, find references, rename, workspace symbols, inlay hints and quick fixes. See Editor support for what each of them does.

One default is worth knowing about. Inlay hints are on, so an unannotated binding or parameter shows the type the compiler inferred, and ?T where inference has not pinned one; accepting a hint writes the annotation into the file wherever that is legal.

Other editors

Any LSP client can drive praxis lsp — it speaks JSON-RPC over stdio and takes no arguments. Clients that append --stdio to the server’s argv are fine: the flag is accepted and ignored, because stdio is the only transport there is.

Running the book’s examples

Every program in this book that is shown together with its output is a real file under docs/book/examples/, and docs/book/examples/verify.sh re-runs all of them against the expectations printed here:

$ ./docs/book/examples/verify.sh getting-started

6 ok, 0 failed

If a chapter and the compiler ever disagree, that script is what says so.