Rue is a statically typed programming language that compiles to CLVM bytecode. It's designed as an alternative to Chialisp for writing coin puzzles on the Chia blockchain.
Docs · Playground · Discord
fn main(public_key: PublicKey, conditions: List<Condition>) -> List<Condition> {
let agg_sig = AggSigMe { public_key, message: tree_hash(conditions) };
[agg_sig, ...conditions]
}- Type safe: Structs, generics, type guards, and flow-sensitive narrowing catch entire classes of mistakes at compile time, before a puzzle ever touches the chain.
- Optimized: The compiler works hard to minimize the size and runtime cost of the generated bytecode.
- Familiar: A simplified, Rust-like syntax that feels more at home for most developers than Lisp.
- Batteries included: A standard library with conditions, tree hashing, currying, singletons, offers, and more.
- Tooling: A built-in test runner, formatter, CLVM debugger, and language server.
Install the CLI from crates.io:
cargo install rue-cliSee the Installation page for more options. If you just want to try the language, the Playground runs entirely in your browser.
Create a new project:
rue init my-project
cd my-projectThis creates a Rue.toml manifest and a puzzles/main.rue entrypoint:
fn main() -> String {
"Hello, world!"
}Compile it to CLVM:
$ rue build
(q . "Hello, world!")| Command | Description |
|---|---|
rue init [path] |
Create a new project with a Rue.toml manifest. |
rue build [file] |
Compile the project (or a single file) and print the CLVM output. |
rue test [file] |
Run every test fn in the project (or a single file). |
rue fmt [paths...] [--check] |
Format Rue source files, or check that they're already formatted. |
rue debug <program> [solution] |
Run a CLVM program against a solution and print the result. |
rue lints |
List every lint and its default level, and the lint groups. |
Useful build flags:
--export <name>- compile a specific exported function instead ofmain--hex- output serialized bytecode as hex--hash- output the tree hash of the compiled program--debug- include debug information such as assertion messages--all- compile every export in the project and write each one to a file (next to its source, or indist_dirif set inRue.toml)
Warnings such as unused bindings or unnecessary casts are lints, which can be configured to be allowed, reported as warnings, denied (which fails compilation), or forbidden (which is like denied, but can't be changed by attributes or later flags). Run rue lints to see every lint and its default level.
Related lints are also in groups, which can be configured anywhere a lint can. For example, unused contains every unused_* lint. Run rue lints to see the groups and the lints in them.
Levels can be set in Rue.toml:
[lints]
unused = "deny"
unused_binding = "allow"
unnecessary_cast = "allow"Within Rue.toml, a lint's own level takes precedence over the level of its group, regardless of the order they're written in. Here every unused_* lint is denied, except for unused_binding. The exception is a forbidden group, whose lints can't be set to any other level, so unused = "forbid" with unused_binding = "allow" is an error.
Levels can also be overridden for a single build or test with -A/--allow, -W/--warn, -D/--deny and -F/--forbid, which take precedence over Rue.toml, including a group flag over a lint set individually in Rue.toml. Among the flags, a lint's own flag takes precedence over its group's, unless the group is forbidden. Otherwise, if a lint is passed more than once, the last flag wins, unless the lint is already forbidden in Rue.toml or by an earlier flag (which is an error):
rue build -D unused -A unused_importLevels can also be set in the source code, with an allow, warn, deny or forbid attribute on an item (such as a function, constant, struct, type alias, import or module) or a statement. The attribute applies to everything inside of what it's attached to, and the innermost one takes precedence over any outer attributes and the project's levels, whether it names a lint or a group. If an item or statement has more than one attribute for the same lint, the last one wins:
#[deny(unused_binding)]
mod strict {
#[allow(unused)]
fn reserved(value: Int) -> Int {
#[warn(unused_binding)]
let unused = 1;
42
}
}A forbid attribute works like deny, but nothing inside of it can change the lint's level. The same goes for lints forbidden in Rue.toml or on the command line. An attribute or flag that tries to change the level of a forbidden lint has no effect on it, and is reported as an error. A group still sets the lints in it that aren't forbidden:
#[forbid(unused_binding)]
fn strict() -> Int {
#[allow(unused_binding)] // Error: Cannot set lint `unused_binding` to `allow`, since it's forbidden by the attribute on line 1
let unused = 1;
42
}An expect attribute allows a lint like allow does, but also expects it to be reported inside of what it's attached to. If it never is, the attribute is reported with the unfulfilled_lint_expectation lint, so it doesn't stay around after the code it was written for changes. An expect for a group is fulfilled by any lint in it, and like allow, it can't change a forbidden lint. As in Rust, expectations aren't checked if there are any errors other than lints (since code that doesn't compile can't be fully linted), and unfulfilled_lint_expectation itself can't be expected:
fn main() -> Int {
#[expect(unused_binding)]
let unused = 1;
42
}To set a level for a whole file, use an inner attribute (with a !) at the start of the file, before any items. Inner attributes can also go at the start of a mod { ... } body to apply to the whole module, where they come after the attributes on the mod itself, so they take precedence unless those forbid the lint. Attributes on the items inside still override them:
#![allow(unused_function)]
fn reserved() -> Int {
0
}
#[warn(unused_function)]
fn checked() -> Int {
1
}Tests are just functions marked with test, and they live right alongside your code:
fn factorial(num: Int) -> Int {
if num > 1 {
num * factorial(num - 1)
} else {
1
}
}
test fn factorial_works() {
assert factorial(0) == 1;
assert factorial(5) == 120;
}rue testThe examples directory contains small programs like FizzBuzz, a JSON parser, and a Brainf*ck interpreter, as well as real-world puzzles such as a CAT, singleton, and royalty split.
Warning
The example puzzles have not been audited and are for educational purposes only.
| Path | Description |
|---|---|
crates/rue-cli |
The rue command line tool. |
crates/rue-compiler |
The compiler driver and the standard library. |
crates/rue-lexer, rue-parser, rue-ast |
Tokenizing, parsing, and the syntax tree. |
crates/rue-hir, rue-types |
Semantic analysis and the type system. |
crates/rue-lir |
Low-level IR, optimization, and CLVM code generation. |
crates/rue-formatter |
The source code formatter. |
crates/rue-lsp |
The language server. |
wasm |
WebAssembly bindings for the compiler. |
playground |
The web-based Rue Playground. |
tests |
Snapshot tests for the compiler. |
Contributions are welcome! The project builds with a stable Rust toolchain (pinned in rust-toolchain.toml).
# Run the full test suite
cargo test --workspace
# Regenerate the .yaml snapshots for tests/ and examples/ (optionally filtered by name)
cargo run -p rue-tests [filter]
# Lint and format
cargo clippy --workspace --all-targets
cargo fmt --all
cargo run -p rue-cli -- fmt 'examples/**/*.rue' 'tests/**/*.rue' 'crates/rue-compiler/src/std/**/*.rue'Each .rue file in tests and examples has a matching .yaml snapshot with its compiled output, runtime cost, and test results. Review snapshot diffs carefully, since they show exactly how a change affects the generated bytecode.
To run the playground locally:
cd wasm && pnpm install && pnpm build
cd ../playground && pnpm install && pnpm devJoin the Sage Discord to get help with Rue, suggest ideas, or chat about the project.
Rue is licensed under the Apache License 2.0.