Skip to main content

CLI & Scripts

quilt — the main CLI

File: quilt/src/bin.rs

The quilt binary has three subcommands; run is the default, so quilt <file> [args…] is shorthand for quilt run <file> [args…].

quilt expand <file.rs.quilt>

Expands a .quilt file to the corresponding source file (strips .quilt suffix).

quilt expand path/to/foo.rs.quilt # produces foo.rs
quilt expand path/to/foo.py.quilt # produces foo.py
quilt expand path/to/foo.wgsl.rs.quilt # produces foo.wgsl.rs

Options:

  • -m, --multi <omni|bootstrap> — select which Multi to use. Default omni. Use bootstrap only when expanding mk_meta.rs.quilt to regenerate meta.rs.

The generated file starts with a comment //! DO NOT EDIT. GENERATED BY quilt expand ....

expand is the one subcommand that does require the .quilt suffix, because the output name is the input name with the suffix sliced off — there is nothing to write without one. A file without it is refused with expected a .quilt file.

quilt check <file.rs.quilt>…

Validates one or more .quilt files without writing anything: each file is parsed and expanded exactly as expand would, but the result is discarded. Prints <file>: ok per valid file, reports errors for invalid ones (checking every file before failing), and exits non-zero if any file fails — useful for CI pipelines and pre-commit hooks where you don't want generated files checked into git.

quilt check path/to/foo.rs.quilt
quilt check src/*.quilt

Options:

  • -m, --multi <omni|bootstrap> — same as expand.

A leading shebang line is stripped before checking (as run does), so executable scripts check clean.

The language chain is derived the same way run derives it, through one shared helper: the path is resolved (following symlinks) and the chain read off the resolved file's own name, with .quilt stripped if it is there. So an extension-less entry point — bin/issues is a symlink to examples/issue_triage.html.py.quilt — checks as Python, and only the file name counts, so a dot in a parent directory can't leak into the chain. check used to require a literal .quilt suffix on the path it was handed, which meant a script the repo ships to be run could never be validated in CI (issue #188). A name that resolves to no registered language still fails, naming the language rather than the suffix.

quilt check bin/issues # an extension-less `#!/usr/bin/env quilt` script

bin/check-examples checks examples/**/*.quilt plus every #!/usr/bin/env quilt script under bin/, which is what keeps the entry points from rotting.

quilt run <file.rs.quilt> [args…]

Expand a .quilt file to a temp file and run it immediately. The file's inner extension determines the runner:

ExtensionRunner
.rsrust-script
.pypython3 (with PYTHONPATH set to include quilt-python)
.tsnode --experimental-strip-types (with a node_modules binding the runtime)

The runner is read from Language::hashbang(). If the language does not return a hashbang, quilt errors.

Trailing arguments after the filename are forwarded to the script.

For Rust scripts, quilt injects a //! [dependencies] cargo manifest block in the generated file pointing at the local quilt crate with the correct feature set.

Since run is the default subcommand, the run keyword can be omitted:

quilt examples/hello.rs.quilt World
quilt examples/hello.py.quilt
quilt examples/staged_pow.ts.quilt
quilt examples/countdown.rs.quilt 5

Binding the runtime

Each host needs its runtime importable, and each language wants that spelled its own way:

  • Rust gets the cargo manifest above.
  • Python gets PYTHONPATH.
  • TypeScript gets a node_modules. Node's ESM resolver ignores NODE_PATH, so a bare import … from "quilt" resolves only against a node_modules directory above the importing file — and a temp file has none. quilt run therefore gives the script a private directory holding a node_modules with two entries, mirroring the browser demos' import map: quiltquilt-wasm/node (the runtime plus ) and quilt-wasmquilt-wasm/pkg (the raw wasm-pack package). Build them first with bin/build-ts; until then quilt run on a .ts.quilt file errors telling you so.

Python and TypeScript runs also inherit $QUILT (the path of the running expander) and, for TypeScript, $QUILT_CHAIN (the file's language chain, ground first). That is what lets re-expand a generated stage — one whose source still contains Quilt glyphs — without needing quilt on PATH. See Reduce on the CLI.

Shebang scripts

.quilt files can be used directly as executables with:

#!/usr/bin/env quilt

as their first line (#!/usr/bin/env quilt also works, but the bare form is portable: Linux passes everything after the interpreter as a single argument, so env would look for a program literally named quilt run). quilt strips the shebang before expansion so the language parser doesn't see #!.

Such a script usually wants no extension at all — bin/issues, a symlink to examples/issue_triage.html.py.quilt. Both run and check resolve the link and take the chain from the target's name, so an entry point can be validated as well as executed.


bin/ scripts

All scripts in bin/ expect the direnv environment to be active (direnv allow from the repo root). They work from any directory when the env is active.

quilt

quilt expand path/to/file.rs.quilt
quilt path/to/file.rs.quilt [args]

A thin wrapper around cargo run -p quiltlang --. Passes all arguments to the quilt binary.

bootstrap

Runs the full two-stage bootstrap that regenerates quilt/src/langs/rust/meta.rs. Both stages run the generator program mk_meta.rs.quilt via quilt, which writes meta.rs; see Bootstrap.

bootstrap

Equivalent to running bootstrap0 then bootstrap1 in order.

bootstrap0

Stage 0 only: quilt -m bootstrap mk_meta.rs.quilt — expands the generator with BootstrapMetaLanguage (built with --no-default-features -F bootstrap) and runs it.

bootstrap0

bootstrap1

Stage 1 only: quilt -m omni mk_meta.rs.quilt — expands the generator with the freshly generated RustMetaLanguage (self-hosted) and runs it. A clean run leaves meta.rs unchanged.

bootstrap1

ctest / lint

Wrappers around cargo test and cargo clippy --tests that work from any directory (they pass --manifest-path for the repo root).

ctest -p quiltlang node
lint

build-py

Build the quilt_python PyO3 extension module. Required before running .py.quilt files. Rebuild after editing the Python bindings.

build-py

Builds with maturin and installs the abi3 module as quilt-python/quilt/_quilt.abi3.so, which Python can import as import quilt.

build-ts

The TypeScript counterpart: build the quilt-wasm runtime for Node. Required before running .ts.quilt files. Rebuild after editing the wasm bindings.

build-ts

Runs wasm-pack build quilt-wasm --target nodejs --out-dir pkg — CommonJS with the wasm instantiated on require, so a CLI run needs no fetch/init dance. (The npm package and the browser demos use --target web, which examples/web/build.mjs puts in pkg-web/; the API surface is the same.) quilt run layers quilt-wasm/node on top, which is what adds .

test-py / test-ts

Exercise each runtime end to end: test-py runs the pytest binding tests and the self-contained .py.quilt examples; test-ts runs the Node tests (quilt-wasm/test/reduce.mjs) and the self-contained .ts.quilt examples. Each needs its runtime built first.

Both also re-run cargo test -p quiltlang --test cli, because the quilt run tests in quilt/tests/cli.rs skip themselves when their runtime isn't built — a plain cargo test stays green without maturin or wasm-pack, so these scripts are the only place the Python and Node runner paths execute for real.

build-py && test-py
build-ts && test-ts

ts-gen

Regenerate the tree-sitter parser after editing tree-sitter-quilt/grammar.js.

ts-gen

install_tools

Build and install the editor tooling: cargo install --path quilt-lsp, npm install for the VS Code extension, and symlink tools/quilt into ~/.vscode/extensions/quiltlang. Idempotent; warns if rust-analyzer or rust-script is missing. See Editor Setup.

install_tools

Cargo workspace commands

Run from the repo root (the Cargo workspace root):

cargo build # build all workspace members
cargo test # run all tests
cargo test -p quiltlang node # run tests matching "node" in the quilt crate
cargo clippy # lint
cargo fmt # format

cargo build -p quilt-lsp # build the LSP server
cargo test -p quilt-lsp # run LSP tests

Testing the CLI

Two suites, splitting the work between the real binary and the rendering:

SuiteCovers
quilt/tests/cli.rsThe real binary, through CARGO_BIN_EXE_quilt: the happy paths and the exit codes — check, expand (sibling file, header comment, chain derivation), run for each of the three runners, -m omni vs -m bootstrap, --help.
quilt/tests/ui.rs + quilt/tests/ui/A corpus of deliberately invalid inputs whose full rendered miette diagnostic is snapshotted — error text, help, and above all where the caret lands.

The ui/ corpus is what regression-tests spans and error wording. Every case must fail with a diagnostic: an input that expands cleanly is reported as an error, so a case that starts passing (because the capability it probes got implemented) fails loudly and asks to be reclassified.

It renders in-process with an explicitly configured GraphicalReportHandler rather than by capturing the binary's stderr — the binary's rendering is miette's global hook, whose width, colour and box-drawing charset are all sniffed from the environment, so a committed snapshot would differ between a laptop and a CI container. cli.rs separately asserts that the real binary renders a snippet with a line:col, which covers the other half.

Adding a case is a fixture in tests/ui/ plus a line in the corpus_is_complete roster saying which error kind it pins — the two are checked against each other, so neither can exist without the other.

cargo test -p quiltlang --test ui # run the corpus
cargo insta review # accept a changed diagnostic

Environment variables

VariableUsed byDescription
RUST_LOGquilt, quilt-lsptracing log filter (e.g. debug, info)
QUILT_LSP_RUST_ANALYZERquilt-lspOverride rust-analyzer command (whitespace-separated)
PYTHONPATHquilt (Python)Extended to include the quilt-python/ directory