Skip to main content

v0.2.3 · Technology preview

Memory safety,without the GC tax.

Riddle eliminates memory errors at compile time with move semantics and borrow checking, and keeps every value that never outlives its frame on the stack via interprocedural escape analysis. Not one lifetime annotation, and no garbage collector running the whole way.

main.rid
struct Point {
    x: i32,
    y: i32,
}

fun distance_squared(point: Point) -> i32 {
    point.x * point.x + point.y * point.y
}

fun main() {
    let point = Point { x: 3, y: 4 };
    let value = distance_squared(point);
    print(value)
}
lifetime annotations
0 lifetime annotations
runtime ABI functions
5 runtime ABI functions
compiler stages
9 compiler stages
backend output
C11 backend output

Language features

Why Riddle

Ownership, escape analysis and deterministic destruction together — safety that needs neither annotations nor an always-on collector.

Ownership is settled at compile time

Assignment, argument passing and returning all transfer ownership. The move checker catches use-after-move, conflicting borrows, and assignment or movement during a borrow — all at compile time, with no runtime checks.

struct Foo {
    x: i32,
    y: i32,
}

fun main() {
    let a = Foo { x: 1, y: 1 };
    let b = a;
    print(a); // error: a has already been moved
    print(b);
}

Scalars, shared references, raw pointers and named function items are copyable by default; &mut T and closure values are not.

  • User types opt into copy semantics by implementing std::marker::Copy; the compiler verifies every field and enum payload
  • Field access alone does not move the whole struct; match destructuring records per-field partial moves, and untouched sibling fields stay usable
  • Option<T> and Result<T, E> are Copy only when every payload type is Copy

Return a reference without writing a lifetime

Riddle has no lifetime syntax. An interprocedural fixpoint escape analysis computes a "leaked parameters / returned-from parameters" summary for every function, and uses it to decide whether a local stays on the stack or is promoted to the conservative non-moving GC heap. Storage location never relaxes move or borrow checking.

struct Foo {
    x: i32,
    y: i32,
}

fun make_ref() -> &Foo {
    let foo = Foo { x: 1, y: 2 };
    &foo // escapes the frame, so foo is promoted to the GC heap
}
Analysis result MIR allocation
Non-escaping local that needs no stable address SSA value, no allocation emitted
Non-escaping local that is mutable or captured by reference Alloca, stack storage
Escaping local HeapAlloc, GC heap storage
Non-escaping / escaping closure environment Alloca / HeapAlloc

The GC only decides addresses — destruction stays deterministic

Types implementing `std::ops::Drop` run their destructor deterministically when the owning scope ends. Escaping to the GC heap changes a value’s address, not when it is destroyed; drop flags make sure a moved-out value is never destroyed twice.

struct FileHandle {
    raw: i32,
}

impl Drop for FileHandle {
    fun drop(&mut self) {
        // release the external resource behind raw
    }
}

Locals, parameters, pattern bindings, iteration elements, aggregate fields and closure environments are all covered.

  • Drop + Copy, calling the destructor directly, and moving a field out of an explicit Drop type are all rejected
  • A for loop’s current element, its iterator and its early-exit paths each get their own drop scope

Abstraction that costs nothing at runtime

Traits support default methods, associated types with defaults, supertrait declarations and transitive bounds. Generics are monomorphized in the C backend, and const generics lift lengths into the type.

trait Summary {
    fun title(&self) -> &str;

    fun summarize(&self) -> &str {
        self.title()
    }
}

struct Buffer<T, const N: usize> {
    data: [T; N],
}

An impl that does not override a method inherits the trait’s default body.

  • <T: A + B> and where clauses; where on an impl is checked against the Paterson condition
  • Operators dispatch to user types through #[lang = "..."] traits; scalar operations lower straight to native C operators with no wrapper functions
  • == / != check PartialEq, ordering comparisons check PartialOrd, and heterogeneous right-hand-side impls are supported

The compiler tells you exactly which arm is missing

`match` performs recursive exhaustiveness checking over enums, booleans, `()`, integers, tuples and structs. A non-exhaustive integer match does not report a vague error — it reports the uncovered contiguous ranges.

fun classify(n: i32) -> i32 {
    match n {
        x if x < 0 => -1,
        0 => 0,
        _ => 1,
    }
}

Exhaustiveness gaps are reported as E0039 together with the uncovered ranges.

  • A failed guard falls through to the following arms; guarded arms do not count toward static exhaustiveness
  • Unit, tuple and struct enum variant patterns are supported, and payload bindings are visible in both the guard and the arm

Talking to C, with the boundary written down

Imports inside an `unsafe extern "C"` block are unsafe by default, and you opt individual functions back in with `safe fun`. The C backend provides no built-in helpers keyed on function names — every `extern "C"` declaration becomes an ordinary external symbol.

unsafe extern "C" {
    safe fun abs(x: i32) -> i32;
    fun malloc(size: usize) -> *mut u8;
}

fun main() {
    let value = abs(-42);
    let pointer = unsafe { malloc(16) };
}

Dereferencing and indexing raw pointers requires an unsafe context.

  • &str maps to const char* in C imports; exported definitions with a body keep the { ptr, len } fat pointer
  • unsafe fun and unsafe fun(...) -> T function types, with one-way safe-to-unsafe conversion

Compilation pipeline

Source to C, in nine stages

riddlec already runs the complete frontend and the C backend. Every stage is covered by tests in the repository.

  1. 01

    Lexing and parsing

    IncrementalParser exposes a partial re-parse API

  2. 02

    AST wrapping

    A uniform syntax tree; attributes travel with their items

  3. 03

    HIR lowering

    Includes the E0040 / E0050 / E0051 / E0052 diagnostics

  4. 04

    Scope graph and name resolution

    Fragment-based incremental scope graph with partial invalidation

  5. 05

    Type checking

    A reusable IncrementalTypeChecker

  6. 06

    Escape analysis

    Interprocedural fixpoint deciding stack versus GC heap allocation

  7. 07

    Move checker

    Use-after-move, conflicting borrows, assignment and movement during a borrow

  8. 08

    MIR lowering

    SSA form with Phi nodes, basic blocks and Alloca / HeapAlloc

  9. 09

    C code generation

    Emits C11 that calls the rgc runtime ABI

With no backend selected, riddlec stops after move and borrow checking; MIR lowering only continues when backend code is actually needed.

Runtime

Five functions make up the runtime ABI.

Every runtime provider implements these five symbols: initialize the stack, allocate, resize and free memory, and trigger collection.

rgc.h
void rgc_init(void *stack_bottom);
void *rgc_alloc(size_t size);
void *rgc_realloc(void *ptr, size_t size);
void rgc_free(void *ptr);
void rgc_collect(void);

crates/gc ships the default conservative, non-moving mark-sweep implementation; point [runtime].source in Clue.toml at your own provider to replace it.

No Boehm GC
clue build compiles the generated C and the runtime source with your system C compiler — no extra third-party runtime dependency.
Conservative and non-moving
Only values that escape analysis proves outlive the current frame reach the GC heap. Everything else stays on the stack, and the collector never relocates objects.
Each allocation hook has a job
rgc_realloc grows Vector buffers and rgc_free releases provider-owned memory; a non-GC provider may ignore the stack bottom and make rgc_collect a no-op.
Fully replaceable
The runtime provider is chosen by clue and accepts custom providers — embedded or specialised targets can bring their own allocator.

What's new in v0.2.3

A more expressive language and a broader library

This release adds bracket lambdas, vec! and range expressions, iterator adapters, new file / time / random / parse modules, and a sharper LSP with nine quick fixes and Clue.toml support.

  1. 01

    Bracket lambdas

    [v -> v * 2] writes a short anonymous function with inferred parameter and return types, and values.map [v -> v * 2] calls it inline. They lower to the same closures as fun; vec![a, b] and 0..5 ranges round out the new syntax.

  2. 02

    A broader standard library

    Collections gained remove, insert and sort; iterators gained lazy map / filter / zip and eager count / fold / find; new std::fs, std::time, std::random and std::parse modules cover files, sleeps, randomness and number parsing.

  3. 03

    A sharper language server

    Nine diagnostic-derived quick fixes, lambda-parameter and method-chain type hints, Clue.toml schema diagnostics with completions, organize imports, expand selection, document links and pull diagnostics.

  4. 04

    Compiler foundations

    riddlec accepts multiple input files, constants evaluate at compile time for array lengths and const generics, slices cross unsafe extern "C" as pointer/length pairs, and the GC uses a hashed object registry with an adaptive threshold.

Toolchain

One manager, four binaries

ridup manages Riddle versions; a toolchain is clue, riddlec, riddle and riddle-lsp. Prebuilt archives are on GitHub Releases; installing from source needs a recent Rust stable.

Selects and runs installed toolchains. The stable, nightly and canary channels each get their own copy and never overwrite one another. It manages Riddle versions only — the selected clue still finds a host C compiler.

ridup toolchain install stable | nightly | canary

  • stable and nightly take the GitHub Release archive and verify its SHA-256 before replacing the old toolchain
  • canary fetches the latest main commit and runs cargo build --workspace --release locally — Rust and Cargo are enough, Git is not needed
  • ridup toolchain link dev <dir> turns a local build directory into a toolchain
  • riddle-toolchain.toml, RIDUP_TOOLCHAIN and clue +dev build all change the selection
  • Copy or hard-link ridup as clue, riddlec, riddle or riddle-lsp and it proxies into the selected toolchain
  • Downloads and Canary builds honour HTTPS_PROXY and the other standard proxy variables
ridup on GitHub

Checks Riddle source and generates C — the frontend checks and the C backend live in one binary.

riddlec [--verbose] [--backend c] [--target <triple>] [--output <file>] <file>...

  • --backend c emits C that calls the rgc ABI
  • --target <triple> selects a target; RIDDLE_TARGET and Clue.toml can set it too
  • Automatically appends std/lib.rid to your source
  • Without a backend it only runs the frontend checks and skips MIR lowering

Provides the unified Riddle tools entry point, currently with source formatting.

riddle fmt [--emit files|stdout|check] [--check] <file>...

  • Formats files or standard input, or checks without writing
  • --tab-size sets indentation width and --hard-tabs selects tabs
  • Shares the formatter with LSP textDocument/formatting

Creates, checks, builds and runs Riddle projects. Binary projects produce a native executable; library projects emit .rlib / .rmeta metadata plus static or dynamic libraries.

clue init | new | check | build | run [--target <triple>]

  • Uses CC strictly when set, otherwise finds a system compiler that can complete a C11 compile and link
  • --target, RIDDLE_TARGET and [build].target in Clue.toml override the host in that order
  • ridup target add <triple> installs the target runtime; linking still needs the target C toolchain
  • Path, git and registry dependencies resolve into a lockfile that pins exact versions
  • clue build keeps .clue/build/<name>.c so you can inspect the output
  • [runtime].source in Clue.toml can point at a custom runtime implementation

Built on tower-lsp. Parse errors, HIR diagnostics, type errors and move / escape diagnostics are all pushed over LSP.

  • Project-wide completion that prefers unsaved content from every open file
  • Semantic tokens that distinguish free functions, methods, structs, enums and traits
  • Error codes link into the error index; notes and fixes are attached as note: / help:

Editor support

In the editor you already use

The editors directory in the repository ships ready-to-use configuration.

  • Helix
  • VS Code
  • Zed
  • IntelliJ IDEA 2026.1+

Working today

  • .rid file recognition
  • Diagnostics across Clue projects, unsaved files and unopened modules
  • Parse, type, move and borrow diagnostics
  • Semantic highlighting for functions, methods, structs, enums, traits, parameters and mutable bindings
  • Inlay hints for locals whose return type comes from another module
  • Cross-file completion including fields, methods, enum variants and associated functions
  • A code action for mutable closure bindings
  • Incremental document sync and semantic token deltas
  • Hover, go to definition and implementation, references, rename and formatting
  • Workspace indexing and automatic imports

Still evolving

  • More extensive semantic refactoring actions

Get started

Five commands to your first program

Install a toolchain with ridup and switch channels whenever you want, or download the archive for your platform from GitHub Releases and add it to PATH.

  1. 1

    Install the toolchain

    Installing a channel does not select it, so pick the default too. ridup show then tells you which toolchain is active and why.

    cargo install --git https://github.com/riddle-lang/ridup
    ridup toolchain install stable
    ridup default stable
  2. 2

    Create and run a project

    clue run performs the same build as clue build, then runs the resulting executable.

    clue new hello
    cd hello
    clue check
    clue build
    clue run

clue build keeps .clue/build/hello.c. When CC is set Clue uses it strictly; otherwise it searches for cc, gcc, clang and their versioned variants — plus clang-cl and cl on Windows. To type clue or riddle directly, copy or hard-link ridup under that name; without that proxy, run ridup run stable clue new hello.

Project status

This is a technology preview, and we say so

The v0.2.3 language and toolchain may still change incompatibly. Here is the honest boundary of what works.

Working today

  • Type checking, the move checker, borrow and escape analysis
  • Generics, const generics, traits and associated types
  • Closures with Fn / FnMut / FnOnce call-capability checking
  • match with recursive exhaustiveness checking
  • for driven by IntoIterator / Iterator
  • unsafe semantics and C FFI
  • Arrays, slices, strings and the Vector standard library
  • Drop, operator overloading and deterministic C backend behavior
  • Tuple types, tuple patterns and tuple enum variants
  • Procedural macros and standard derives including Default, Hash and Ord
  • Formatted output with positional, named and debug placeholders
  • Bracket lambdas [v -> v * 2] alongside fun closures
  • Iterator adapters, collection removal and vec!
  • File, time, random and parse modules in the standard library
  • riddle fmt source formatting CLI sharing its formatter with the LSP
  • Cross-target builds and target runtimes for seven supported triples
  • A built-in standard library, C11 codegen, project tooling and an LSP

Current limitations

  • Formatting supports positional, named and :? specifiers; width, alignment and fill are not yet implemented
  • Floating-point remainder is unsupported; Rem / RemAssign are integer-only for now
  • Generics lean on monomorphization and do not yet cover all of Rust’s generic power
  • The C backend is the only backend; producing an executable relies on a system C compiler
  • Escape analysis works at whole-local granularity, without field-level splitting
  • Closures capture whole bindings, without field-level precision
  • There is no bare function-pointer type; callables are declared with impl Fn(...) -> T or dyn Fn(...)

Syntax and ABI stability are not guaranteed. Treat Riddle as a language worth trying seriously — not one to ship to production.

Roadmap

What comes next

This roadmap is the project’s actual priority order: correctness first, language capability second, then toolchain and ecosystem. It reorders with feedback and promises no dates.

01 · Near term

The language core

Correctness first — finish the foundations of the safety semantics.

  • Labeled break / continue, and or / range / slice patterns
02 · Mid term

Standard library and toolchain

As the language can express more, the library and the tools have to keep pace.

  • A larger standard library: buffered IO, full format specifiers and a richer parsing surface
03 · Long term

After the core stabilizes

These are deliberately sequenced after the core semantics settle — no head start.

  • Concurrency and async / await
  • Backends beyond C
  • The stability promise for syntax and the runtime ABI

Want to shift the priorities? Tell us on GitHub Issues which item you need most.

About the author

One person, one language

zi2ven

zi2ven

Author and sole maintainer of Riddle

Riddle is Best

From the first line of riddlec to the page you are reading — the compiler, clue, riddle-lsp, ridup, the standard library, the docs and the Playground — everything comes from the same pair of hands.

Being a one-person project is also why it believes in writing the boundaries down: what works and what does not yet, stated plainly on this site and in the docs. If something breaks while you try it, file an issue on GitHub.

Find zi2ven on GitHub

Write a few lines of Riddle

Nothing to install — compile and run it right in your browser.