varyk GitHub

Simple services. Rust underneath.

Build backend services simply. Ship Rust binaries.

Varyk is a small language for APIs, workers, and microservices. No garbage collector, no lifetime annotations, no borrow ceremony, and Rust and crates.io underneath. You write Go-like application code and ship one native binary, checked by the Rust compiler.

Install with Cargocargo install varyk --locked

users.vr

struct User {
    id: i64,
    name: string,
    created_at: Time,
}

struct NewUser {
    name: string,
}

struct State {
    db: sql::Pool,
}

async fn get_user(id: i64, state: Shared<State>) -> Result<Option<User>, Error> {
    state.db.first("select id, name, created_at from users where id = $1", id).await
}

async fn create_user(user: NewUser, state: Shared<State>) -> Result<http::Response, Error> {
    if user.name.is_empty() {
        return Err(http::bad_request("a user needs a name"));
    }
    let created: User = state.db.one("insert into users (name, created_at) values ($1, $2) returning id, name, created_at", user.name, Time::now()).await?;
    let mut r = http::Response::json(created);
    r.set_status(201);
    Ok(r)
}

async fn open_db() -> Result<sql::Pool, Error> {
    let db = sql::connect("sqlite://users.db?mode=rwc").await?;
    db.run("create table if not exists users (id integer primary key, name text not null, created_at text not null)").await?;
    Ok(db)
}

async fn main() {
    let port: u16 = 3000;
    let db = match open_db().await {
        Ok(db) => db,
        Err(e) => {
            log::error("cannot open the database: {}", e);
            return;
        }
    };
    let mut app = http::App::new(Shared::new(State { db: db }));
    app.get("/users/{id}", get_user);
    app.post("/users", create_user);
    if let Err(e) = app.serve(port).await {
        log::error("cannot serve: {}", e);
    }
}
Fig. 1 A users API on a database in one file, the service of milestone 5b4's design, with the created_at: Time of milestone 5c's design; it runs with the varyk-http package, and varyk check checks each route against its handler. http and sql are the keys varyk add http sql gives the varyk-http and varyk-sql packages, named as any Varyk package is since milestone 5b2; varyk-sql arrived with milestone 5b3, and varyk-http with milestone 5b4. Error and log are here since milestone 5a, async and Shared since milestone 5b1, and Time since milestone 5c. See the roadmap.

What a service needs. What Rust guarantees.

Rust underneath gives you all of this today. The batteries that make a service, what a Go or TypeScript team expects to find built in, are listed below them: JSON, configuration, logging, and tests arrived in milestone 5a, async in milestone 5b1, packages that use packages in milestone 5b2, facades for packages in milestone 5b3, routes checked against their handlers in milestone 5b4, and times, ids, and bytes in milestone 5c; databases and HTTP come as packages, varyk-sql since milestone 5b3 and varyk-http since milestone 5b4, and MongoDB as varyk-mongo, after milestone 5.

The batteries

JSON, configuration, logging, and tests are in the compiler since milestone 5a, async since milestone 5b1, and Time, Uuid, and Bytes since milestone 5c; the examples show them. Databases and HTTP come as packages you add, varyk-sql, varyk-mongo, and varyk-http, so a program that prints a line never compiles a web server or a database driver. Milestone 5b3 gave the compiler what varyk-sql needs, and milestone 5b4 what varyk-http needs; varyk-mongo came after milestone 5. Each package lives in its own repository and is released on its own. The figure at the top of the page is a service with varyk-http and varyk-sql.

Varyk exists so that ordinary backend services can be written simply and shipped as safe Rust. It is for the application: kernels, database engines, and borrow-heavy libraries stay in Rust, in a .rs file beside your Varyk, in the same build. Read why Varyk exists.

Varyk packages are Cargo packages.

A Varyk package is a Cargo.toml and a src/main.vr. varyk init writes one, varyk run builds it, and varyk publish ships it to crates.io as an ordinary Rust crate that needs no Varyk to use. Since milestone 5b2, a Varyk package uses another Varyk package by its key in Cargo.toml, with no facade, and varyk builds every one of them from its .vr sources, never from Rust its publisher shipped. There is nothing to bootstrap: every crate on crates.io was Varyk's ecosystem from the first day, and every Varyk package joins it.

  1. main.vr Your service, in Varyk. It never names a Rust crate, and it never writes a reference.
  2. text.rs A Rust file beside it: a facade that wraps what the program needs in plain functions, structs, and enums, which Varyk imports like a module of its own. Since milestone 5b3, a facade can also read its result into whatever type the caller names and take any number of values, and since milestone 5b4 take any value JSON can write, so a package such as varyk-sql keeps its Rust in a facade, and a program that uses it writes no Rust.
  3. crates.io Any crate, added with varyk add or cargo add. Need Stripe, AWS, or Kafka? Use the Rust crate.

Need something Varyk doesn't provide? Use the Rust crate. Need Rust itself, for the one hot loop or the borrow-heavy library? Write it in the .rs file, in the same build, and call it from Varyk. The matcher example wraps regex-lite this way, and the tools page describes init and publish.

How it works.

Most of Rust's surface is the spelling of decisions the compiler can make on its own. Varyk keeps Rust's ownership model inside the compiler and takes the spelling out of the language. This program runs today; everything marked is what the compiler wrote for you.

borrowing.vr

struct User {
    name: string,
}

fn rename(mut user: User) {
    user.name = "Bob";
}

fn print_user(user: User) {
    println!("{}", user.name);
}

fn main() {
    let mut user = User {
        name: "Alice",
    };

    print_user(user);
    rename(user);
    print_user(user);
}

generated Rust

#[allow(warnings, arithmetic_overflow, unconditional_panic)]
#[derive(Clone, PartialEq)]
struct User {
    name: String,
}

#[allow(warnings, arithmetic_overflow, unconditional_panic)]
fn rename(user: &mut User) {
    user.name = "Bob".to_string();
}

#[allow(warnings, arithmetic_overflow, unconditional_panic)]
fn print_user(user: &User) {
    ::std::println!("{}", user.name);
}

#[allow(warnings, arithmetic_overflow, unconditional_panic)]
fn main() {
    let mut user = User { name: "Alice".to_string() };
    print_user(&user);
    rename(&mut user);
    print_user(&user);
}
Fig. 2 The Rust that varyk build generates from borrowing.vr, as the compiler writes src/main.rs. Marked: everything the compiler writes for you. The program prints Alice, then Bob.
RuleIn Rust you writeIn Varyk you write
Functions borrow by default. The signature carries the contract.fn rename(user: &mut User)fn rename(mut user: User)
Call sites never write &. No choosing between x, &x, and &mut x.rename(&mut user);rename(user);
One string type. The compiler decides the representation.name: "Alice".to_string()name: "Alice"

From your source to the binary:

  1. .vr Varyk source, with .rs files beside it in the same build.
  2. varyk build Parses, type-checks, and runs borrow analysis. Diagnostics carry codes and fix-its.
  3. target/varyk/ A Cargo project of readable Rust. It is yours, so there is no lock-in.
  4. cargo rustc and the full borrow checker. Varyk never uses unsafe to get around it.
  5. A native binary No garbage collector, no reference counting, no runtime beyond Rust's.

Wherever you’re coming from.

Building services in Go, TypeScript, or Python

  • Application code as simple as Go: the compiler does the memory bookkeeping that Rust asks you to write by hand.
  • Native speed and memory safety with no garbage collector: no pauses, no memory ceiling to tune, and one small binary to deploy with no language runtime to install.
  • The bugs Go and TypeScript compile, a missing value, an unhandled error, two threads on one piece of data, rustc rejects.
  • async/await that looks like JavaScript, with a built-in runtime, since milestone 5b1.
  • One toolchain and one package registry, inherited from Cargo and crates.io, since milestone 3.

Coming from Rust

  • Rust syntax and idioms: immutability by default, let mut, struct, impl, match, Option, Result, and ?.
  • The same safety model: the generated Rust is checked by rustc, and Varyk never bypasses it.
  • Less ceremony for application code.
  • .rs files next to .vr files in the same package, built by the same cargo.
  • Varyk packages are Cargo packages and publish to crates.io as ordinary crates, since milestone 3.

Written with a coding agent

  • Agents already write good code; the slow part is reviewing it. Varyk is designed to keep reviews short.
  • A small language with one way to do each thing, so agent output looks like the code around it.
  • Concrete code, with few abstractions to see through.
  • rustc checks memory safety and data races, so a review can focus on what the service does.
  • A reference that fits in a prompt, and JSON diagnostics with codes and fix-its, so the agent fixes its own mistakes first.
  • One official stack for HTTP, JSON, and databases, the same in every Varyk service (JSON since milestone 5a; databases and HTTP as the packages varyk-sql, since milestone 5b3, and varyk-http, since milestone 5b4, and MongoDB as varyk-mongo, after milestone 5).

Ordered milestones, no dates.

  1. Milestone 1, complete. Compiler skeleton and the borrow-by-default proof: six example programs, diagnostics with codes and fix-its, and varyk check, build, and run.
  2. Milestone 2, complete. Enums, match, for, methods, Option, Result, Vec, ?, and format!.
  3. Milestone 3, complete. Packages and interop: Cargo.toml, Cargo dependencies through .rs facades, nested modules, use, private fields, Rust structs and enums imported from .rs files, varyk init, and publishing to crates.io. Released as 0.1.0.
  4. Milestone 4, complete. Closures, iterators, and patterns: chains such as filter and map, if let and while let, nested, literal, and range patterns, HashMap, as, .clone() and == on structs and enums, and getters that return part of their value with no copy. Released as 0.2.0.
  5. Milestone 5a, complete. Data, configuration, logging, and tests: a built-in Error, json::parse and json::stringify, env::parse with .env, log calls, the attributes #[rename], #[default], #[skip], and #[test], and varyk test and varyk add. Released as 0.3.0.
  6. Milestone 5b1, complete. Async: async fn and .await on a built-in runtime, calls started as tasks, Task::all and Task::all_settled, and Shared for a value many tasks read. Released as 0.4.0.
  7. Milestone 5b2, complete. Packages: Varyk code uses a Varyk package by its key in Cargo.toml, to any depth, and varyk builds every package from its .vr sources, never running Rust a publisher shipped or a build script. Released as 0.5.0.
  8. Milestone 5b3, complete. Facades for packages: a .rs facade function that reads its result into whatever type the caller names, takes any number of values after its other arguments, or takes only text written in the program, plus pub use and varyk add sql. Released as 0.6.0, with the database package varyk-sql, in its own repository, released as 0.1.0.
  9. Milestone 5b4, complete. HTTP: routes and hooks on an app of the varyk-http package, each route checked against its handler by varyk check, an optional status on Error, app.request for tests, and varyk add http sql. Released as 0.7.0, with the HTTP package varyk-http, in its own repository, released as 0.1.0: a users API on a database is varyk init, varyk add http sql, one file, and varyk run away.
  10. Milestone 5c, complete. Time, ids, and bytes: Time, Uuid, and Bytes as built-in types, through JSON, database values, and facades, and Time and Uuid through configuration and route parameters, so the users API does not send a date as a string. Released as 0.8.0, with varyk-sql 0.3.0, which stores the three as native columns, and varyk-http 0.2.0, which reads times and ids from routes and sends and receives bytes; with them, milestone 5 is met.
  11. Milestone 6, in progress. Tooling and beyond: varyk fmt, a language server, more of Rust imported from .rs files, and a decision on a native backend. The first of it came as 0.9.0: varyk lsp, which shows errors and quick fixes in an editor when a file is opened or saved, and a varyk-std line that names the oldest varyk a package needs, so a varyk release no longer stops a published package from building. Not scheduled.

After milestone 5 came the next packages, each in its own repository: the MongoDB package varyk-mongo, released as 0.1.0, and varyk-sql 0.4.0, with $1 placeholders on every database. Varyk 0.8.1 added what varyk-mongo needs, and 0.8.2 added varyk add mongo. varyk-redis is next.

The roadmap has every item. None of it has a date, and none of it is a promise of a release.

Install. Write. Run.

Varyk requires a stable Rust toolchain, 1.88 or newer, installed through rustup. Install the compiler, write a package, and run it:

$ cargo install varyk --locked
$ varyk init hello
$ cd hello
$ varyk run
Hello, world!

init wrote Cargo.toml, .gitignore, .dockerignore, and src/main.vr, the program. For colour and errors in your editor, install the Varyk extension in VS Code, or in Zed once Zed lists it (set up your editor). Continue with getting started, which ends with a first service on HTTP and a database, and the language reference. Source and issues are at github.com/Varyk-Lang/varyk.