varyk GitHub

Language reference

This is the compiler repository's language reference, copied at commit b04bc1e, released as 0.9.0.

This page describes everything Varyk accepts today, in milestone 5c of an experimental, pre-1.0 language (see roadmap for what comes next). Anything not described here is rejected with an error that names what is not supported. For the reasons behind the design, see design page.

Varyk code is compiled to Rust. You do not need to know Rust to read this page. Notes marked "For Rust readers" say what the generated Rust looks like and can be skipped.

Files and modules

A Varyk program is a file ending in .vr. This is the entry file, and it must define a function called main that takes nothing and returns nothing or a Result whose error is Error, such as Result<bool, Error>. The program starts there.

// main.vr
fn main() {
    println!("Hello, world!");
}

A main that returns a Result may use ? (see Passing errors on). When it gives Ok, whatever the value, the program exits with code 0; when it gives Err(e), the program reports the message of e once and exits with code 1. A program that logs (see Logging) reports it as a log line at the error level; any other prints error: and the message to standard error. Nothing may call such a main (V0106).

For Rust readers: that main is written as fn varyk_main(), and the Rust fn main() -> std::process::ExitCode calls it (through varyk_std::run when it is async) and matches on the result; it never calls std::process::exit, so the runtime shuts down and the log is flushed.

A program can also be a package: a directory with a Cargo.toml (the file Rust's build tool, cargo, reads) and a src/ directory whose entry file is src/main.vr (a program) or src/lib.vr (a library), never both. A library must not define main and cannot be run. Cargo.toml says the package's name (ASCII letters, digits, -, and _, starting with a letter or _, not cache or package, which Varyk's build directories use, and for a program not deps, examples, build, or incremental in any case, which cargo's do) and version, and edition = "2024" is required:

[package]
name = "greeting"
version = "0.1.0"
edition = "2024"

[dependencies]
regex = "1"

[dependencies] names the Rust crates the package uses, which its .rs modules can call (Varyk code cannot, see use below), and the Varyk packages it uses, which Varyk code names (see Packages). A path to a crate on disk may be relative to Cargo.toml. Taking a setting from a Cargo workspace (workspace = true), dependencies for only some platforms ([target.'cfg(..)'.dependencies]), and any Cargo target table but one ([[test]], [[example]], ...) are errors. The one a package may have names its own root: [[bin]] with name the package's name and path = "src/main.vr", or [lib] with path = "src/lib.vr" and, if present, name the package's name with - as _, and no other key (a package without one is V0406). A dependency key std, core, or alloc is an error too, since it would hide Rust's standard library; package = ".." renames it. check reads a fixed set of Cargo.toml keys, the ones a small service needs (the tables package, workspace, dependencies, dev-dependencies, build-dependencies, target, features, profile, patch, replace, lints, and badges, and under [package] the standard keys cargo documents, apart from the target-layout ones, auto* and default-run); any other key is V0401, "not supported yet", so nothing unknown can pass check and surprise build or publish. Also errors are links, which needs a build script that no crate Varyk builds runs, a build key other than build = false (the crates Varyk builds run no build script), [lints], which would apply to the generated Rust too (put #[deny(..)] or #[warn(..)] attributes in the .rs file instead), and [patch] or [replace], in the package or in the root manifest of an enclosing workspace, which would not apply to the crate varyk build makes. That crate, and the one varyk publish assembles, are built with your Cargo.toml as it is, apart from what the generated layout needs: build = false, no autobins, autolib, include, exclude, default-run, or workspace key, an empty [workspace] table, and relative paths in the dependency tables made absolute. So [features], [profile.*], rust-version, and the rest mean to Varyk exactly what they mean to cargo. For a workspace member (an ancestor Cargo.toml with a [workspace] table that does not exclude the package, or the directory [package] workspace names), the root's Cargo.lock, [profile.*], and resolver are used, as cargo uses them. check reads [dev-dependencies] only for its names (a .rs module using one is told to move it to [dependencies]). A Cargo.lock beside Cargo.toml is used when building, so Varyk builds the same crate versions cargo would.

Which one a file is follows one rule: look for Cargo.toml in the file's directory and then upward; if the file is that package's src/main.vr or src/lib.vr, it is the package, and anything else is a program of its own. So varyk run src/main.vr inside a package builds the package, and varyk run examples/hello.vr inside a Rust project stays a single file.

A program can be split into modules. mod math; in the entry file loads the module math from math.vr or math.rs in the same directory, or from math/mod.vr. A .rs file is Rust code; see "Calling Rust" below. Having more than one of these files is an error, and a module cannot be called main or lib. A module of the entry file cannot be called bin either, because cargo treats the files in src/bin/ as extra programs; a bin further down is fine. These names are refused in any capitalization (Main, LIB, Bin), since on a file system that ignores case, as macOS and Windows do by default, Main.rs is the same file as main.rs.

Any .vr module can declare modules of its own, to any depth. The files of a module's modules live in a directory named after it: mod cart; in shop.vr loads shop/cart.vr or shop/cart.rs, and so does mod cart; in shop/mod.vr (use shop/mod.vr when shop is a directory and you want no shop.vr beside it; having both is an error). A .rs module cannot declare modules. pub mod cart; declares a public module.

A path names an item through modules, as deep as they go: shop::cart::Cart starts at a module declared in the current file, crate::shop::cart::Cart starts at the entry file, self::cart::Cart at the current module, and super::Order at the module that declares the current one (super in the entry file is an error). Paths work the same way for types, calls, struct literals, enum variants, and patterns.

use path; makes a local alias for a module, struct, enum, or function inside the package, so you can write a shorter name afterward; use path as name; gives it a different local name instead. The path starts the same way a path anywhere else does: crate::, self::, super::, or a module declared in the current file, never a name another use made (write use a::g as gg;, not use a as aa; then use aa::g as gg;). With a module shop holding a module cart:

// shop.vr
pub mod cart;
// shop/cart.vr
pub struct Cart {
    pub n: i32,
}

impl Cart {
    pub fn new() -> Cart {
        Cart { n: 0 }
    }
}
// main.vr
mod shop;

use crate::shop::cart::Cart;
use shop::cart;

fn main() {
    let a = Cart::new();
    let b = cart::Cart::new();
    println!("{} {}", a.n, b.n);
}

A use cannot end at an enum variant (write Shape::Circle, not a use for Circle alone) or at a function of a type (call Cart::new()). Nor can it start with the name of a crate: Varyk code does not use crates directly, so use std::collections::HashMap; is an error even though std is built into Rust, and so is use regex::Regex; in a package whose [dependencies] names regex, and so is a path starting at it; call the crate from a .rs module in the package instead (see "Calling Rust" below). A Varyk package in [dependencies] is different: its key names it, as "Packages" below describes. A leading name this compiler does not recognize as a crate is reported as an unknown module instead, with a note in case it was meant to be one. A use name that is already taken by something of the same kind (a type or module, or a function) declared in the same file, or by a built-in type or one of Some, None, Ok, Err, Option, Result, or Vec, is an error too. When the name is both a function and a struct or enum (or a module) where it is declared, the use brings in all of them, so any of them can be the one that is already taken.

pub use path; does what a use does and also re-exports the item: a function, struct, or enum of the same package then has a second name, in the module the pub use is in, beside its own path. A library's root can gather what its users need:

// src/lib.vr of a package `shelf`
pub mod store;

pub use store::item;
pub use store::Book;

A program that uses shelf writes shelf::item(3) and shelf::Book, and may still write shelf::store::item(3). The item must be pub, and so must every module on its own path from the root (pub mod store; above), so the types it names stay visible to its users; otherwise it is an error. A name the module already declares or brings in with a use is taken, as for a use. A pub use of a module, of an item of another package, or with as is not supported yet. For Rust readers: the generated Rust has pub use crate::store::item; in the module, and every path through the re-export is written as the item's own path, ::shelf::store::item(3).

An item or module declared without pub can be used in the module that declares it and in the modules inside that one, and nowhere else: a private function of shop works in shop/cart.vr, but not in the entry file or in another module beside shop. A path works only if every module on it can be used from where it is written, so a pub fn inside mod cart; (not pub mod) of shop is out of reach of the entry file; write pub mod cart; to open it. A pub function, struct field, or enum variant cannot name a type that some of its users could not name themselves, such as a type of shop's private module cart in a pub fn of shop; make the module pub mod, or the function private. In a library the packages that use it are users too: a pub item whose modules are pub all the way up cannot name a type in a private module, and neither can a pub function, method, or field of a .rs module those packages can reach. From outside a module, use its pub items through the module name:

// math.vr
pub fn square(x: i32) -> i32 {
    x * x
}
// main.vr
mod math;

fn main() {
    println!("{}", math::square(7));
}

A top-level item is a function (fn), a struct (struct), an enum (enum), an impl block, a module declaration (mod name;), or a use, each optionally preceded by pub (except impl; pub use is below). A struct's fields follow the same visibility rule as everything else, each on its own (see "Types" below); all variants of a pub enum are public. Only the entry file of a program may define main.

A pub struct or pub enum of a module is named from the file that imports it through the module name, as a type and in a struct literal:

// m.vr
pub struct User {
    pub name: string,
}
// main.vr
mod m;

fn make() -> m::User {
    m::User { name: "Bob" }
}

fn main() {
    println!("{}", make().name);
}

Comments, keywords, and reserved words

A comment starts with // and runs to the end of the line.

The keywords are fn, pub, let, mut, struct, mod, if, else, while, break, continue, return, enum, impl, match, for, in, self, crate, super, use, as, async, await, true, and false. self, crate, and super also start a path: self::, crate::, and super::. as names a use (use path as name;) and converts a number (n as i64, see Expressions and operators). async comes before fn and await after a . (see Async functions and tasks); no keyword can be used as a name.

Every other Rust keyword is reserved: const, dyn, extern, loop, move, ref, Self, static, trait, type, unsafe, where, abstract, become, box, do, final, gen, macro, override, priv, try, typeof, unsized, virtual, and yield. Using one is an error that says the construct is not supported yet. None of them can be used as a name.

Some, None, Ok, Err, Option, Result, Vec, HashMap, and String are reserved names: no function, method, struct, enum, variant, field, module, parameter, or let name can be any of them, because the generated Rust would then hide Rust's own. A struct, enum, or module also cannot take a built-in type's name (i32, string, str, and so on); a function, field, or local can (let string = "x"; is fine). Error, the standard error type, Task and Shared, the standard types of async code, and Time, Uuid, and Bytes, the standard types for a point in time, an id, and a run of bytes, cannot name a struct, enum, module, or use, nor a pub struct or enum of a .rs module (V0113). json, env, log, and time, the standard modules, cannot name a module, a struct, an enum, or a use, and use json; or use json::parse; is an error: a standard module is reached by its path where it is used. assert and assert_eq cannot name a function, and no function, method, struct, enum, module, or use name can start with varyk_, which is kept for what Varyk adds to the Rust it writes (all V0113). A local or a field may use any of these names.

Types

TypeWhat it holds
booltrue or false
i8, i16, i32, i64whole numbers that can be negative, in 8, 16, 32, or 64 bits
u8, u16, u32, u64whole numbers that cannot be negative
usizea whole number that cannot be negative: a length, an index, or a count
f32, f64numbers with a fractional part
stringtext
a struct you declarea group of named fields
an enum you declareone of several variants, each with its own values
Option<T>, Result<T, E>, Vec<T>Rust's standard types, written as in Rust and nested freely
HashMap<K, V>values of type V found by keys of type K, an integer type, bool, string, or Uuid
Errorthe standard error: a message, given by every standard call that can fail
Shared<T>a handle many tasks read one struct T through; written only as a parameter's or a let's type (see Shared)
Timeone point in time in UTC, to the microsecond (see Time, ids, and bytes)
Uuida 128-bit identifier (see Time, ids, and bytes)
Bytesan immutable run of bytes (see Time, ids, and bytes)

A struct declares its fields and their types. A field is private unless marked pub, and follows the same rule as any other item without pub: usable in the module that declares the struct and the modules inside it, and nowhere else. Making the struct itself pub does not make its fields pub; the two are independent, as in Rust:

pub struct User {
    pub name: string,
    age: i32,
}

A struct value is written with its name and every field, in any order: User { name: "Alice", age: 30 }; a literal written outside the struct's module needs every field visible from there. Fields are read and changed with a dot: user.name. Reading, changing, or naming a private field from outside its module is an error suggesting either pub on the field or a pub fn on the struct that does the work instead.

An enum lists its variants. A variant is a plain name, carries values of the types in parentheses, or carries named fields in braces, written like a struct's:

enum Shape {
    Circle(f64),
    Rect(f64, f64),
    Point,
}

enum Event {
    Click { x: i32, y: i32 },
    Key(string),
    Quit,
}

A variant's fields are as visible as its enum, as in Rust, so they take no pub; two fields of one name in a variant are an error.

An enum cannot contain itself, directly or through a struct, another enum, or an Option or Result; inside a Vec or a HashMap it can: enum Tree { Node(Vec<Tree>) }.

An enum value is written with the enum's name and the variant, followed by the variant's values in parentheses when it has any: Shape::Point, Shape::Circle(2.0), Shape::Rect(1.0, 2.0). An enum of another module is reached through it: geo::Shape::Point. The number of values must match the variant, so Shape::Circle(), Shape::Point(1.0), and Shape::Circle without its value are errors, as is a variant the enum does not have. A variant with named fields is written like a struct value, with every field once, in any order: Event::Click { x: 1, y: 2 }; a missing field, an unknown one, or one written twice is an error, and so are parentheses on it or braces on a variant with values by position.

Option, Result, Vec, and HashMap need exactly their types: Option<i32>, Result<User, string>, Vec<Option<Shape>>, HashMap<string, i32>. A HashMap's key type is an integer type, bool, string, or Uuid; its value type is any type. A string inside them is an owned String in the generated Rust, and a HashMap is Rust's std::collections::HashMap. Their values are written as in Rust:

let some = Some(5);               // Option<i32>
let none: Option<i32> = None;
let ok: Result<i32, string> = Ok(1);
let failed: Result<i32, string> = Err("no");
let numbers = vec![1, 2, 3];      // Vec<i32>; every element has one type
let empty: Vec<string> = vec![];
let ages: HashMap<string, i32> = HashMap::new();

Some(x) and vec![a, b] know their type from what is inside. None, Vec::new(), HashMap::new(), an empty vec![], Ok(x) (its error type), Err(e) (its value type), and s.parse() take their type from where they go: a let with a written type, a parameter, a return value, a field, or a vec! element after one whose type is known. Anywhere else, write the type in a let first; the compiler never works it out from later lines.

Numbers, bool, Time, and Uuid are called Copy types: using one makes a copy, and the original stays usable. string, structs, enums, Option, Result, Vec, HashMap, Error, and Bytes are not Copy.

Copies and comparisons. A struct or enum can be copied with x.clone() when every field and value it holds can be: a number, a bool, a string, an Error, a Time, a Uuid, a Bytes, an Option, Result, Vec, or HashMap of such types, another struct or enum that can be copied, or a Rust type whose .rs file derives Clone for it (see Rust structs and methods). Two values of a type can be compared with == and != by the same rule, with PartialEq in place of Clone. A type that holds itself through a Vec or a HashMap counts as if it could, as in Rust. x.clone() is a new, owned copy of everything inside, but for a Bytes, whose clone shares its buffer (see Time, ids, and bytes). It works on an Error and a Bytes too, and on an Option, Result, Vec, or HashMap whose contents can be copied; on a Copy type (a number, bool, Time, or Uuid) it is an error (V0100), since those are copied on use already. When something inside is in the way, .clone() and == are errors (V0203) that name the field, and, for a Rust type, say to derive the trait in its .rs file. In the generated Rust, a struct or enum carries one #[derive(Clone, PartialEq)] line listing what it allows (none when it allows neither); nothing is ever copied unless .clone() is written.

An impl block adds functions to a struct or enum declared in the same file. A function whose parameter list starts with self is a method; self is the value it was called on, borrowed to read, and mut self borrowed to change, like any other parameter. A function without self is an associated function. A type may have several impl blocks, but not two functions of one name. A method or associated function is used from another module only when it is pub.

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }

    fn add(mut self, by: i32) {
        self.count = self.count + by;
    }
}

A method is called on a value, counter.add(2), and an associated function through its type, Counter::new(), or m::Counter::new() for a type of module m. The value a method is called on is passed like any other argument: a mut self method needs a value that may be changed (a let mut name, a mut parameter, a field or element of one, or a fresh value), and the compiler suggests where to add mut when it is not.

In the generated Rust, self is &self and mut self is &mut self.

Calls on built-in types

These are all the calls Vec, string, Option, Result, HashMap, and Error have, beside the calls of a chain (see Chains) and those of Time, Uuid, and Bytes (see Time, ids, and bytes); any other is an error listing the type's calls. "Reads" borrows the value the call is made on, "changes" needs a value that may be changed, like a mut parameter, and "takes" uses the value up, as ? does (see below). An argument is kept, like a struct field, unless the table says it is read: a read argument is only borrowed, so a parameter or another name for a value can be passed.

Vec<T>:

CallUses the valueResult
Vec::new()nonean empty Vec; its type comes from where it goes, as for vec![]
v.push(x)changesnothing; x is kept in v
v.pop()changesOption<T>: the last element, taken out, or None
v.len()readsusize, the number of elements
v.is_empty()readsbool
v.insert(i, x); i: usizechangesnothing; x is kept at i, and the elements from i on move up one
v.remove(i); i: usizechangesT: the element at i, taken out
v.contains(x); x readreadsbool; T is a number type, bool, string, Time, Uuid, or Bytes
v.sort()changesnothing; T is an integer type, bool, string, or Time
v.join(sep); sep: string readreadsnew text: the elements with sep between them; T is string
v.get(i); i: usizereadsOption<T>: the element at i, or None past the end; looked into where it is made unless T is a Copy type (see below)
v.iter()reads; v must be storeda chain of the elements (see Chains)
v[i]reads, or changes when assignedthe element at i, which is a usize

push and insert keep what they are given, like a struct field: a literal string is copied in, a name holding its own value is given away, and a value the function only borrows is an error. v[i] is a place, like a field: it can be read, assigned (v[0] = 5;), have its fields read or assigned, and have methods called on it (tasks[0].complete()). With i past the end, v[i], insert, and remove stop the program with Rust's message. sort on floats, Uuids, or Bytes, or contains or sort on structs, is an error, and so is join on anything but strings.

string:

CallUses the valueResult
s.len()readsusize, the length of the text in bytes
s.clone()readsa new copy of the text
s.is_empty()readsbool
s.contains(p), s.starts_with(p); p: string readreadsbool
s.to_uppercase()readsnew text
s.replace(from, to); both string, readreadsnew text
s.trim()reads; s must be stored, or a literalthe text of s without the spaces at either end: part of s, not a copy (see "Returning part of a parameter")
s.split(sep); sep: string readreads; s must be stored, or a literala chain of the pieces of s between the places sep appears, each part of s, not a copy (see Chains)
s.push_str(t); t: string readchangesnothing; t is added to the end of s
s.parse()readsResult<T, Error>, T a number type, bool, Time, or Uuid

s.clone() is the one way to copy text, and it always makes owned text. parse gives an Err when the text is not a value of T, with Rust's rules for what the text may look like, and its message names the text and the type (`abc` is not a number, `yes` is not `true` or `false`). A Time reads as Time::from_iso reads it, and a Uuid only in its 36-character form with hyphens, hex digits in either case (see Time, ids, and bytes); parse into Bytes, or any other type, is an error (V0200). T comes from where the result goes, as None's type does, ? included: let n: Result<i32, Error> = text.parse();, or let n: i32 = text.parse()?; in a function returning Result<_, Error>. For an Option, write two statements, let parsed: Result<i32, Error> = text.parse(); and then parsed.ok(); text.parse()? in a function returning an Option is an error (V0206), and so is one in a function whose error type is not Error.

Error:

CallUses the valueResult
Error::new(text); text: stringnonea new Error with the message text; text is kept, like a struct field
Error::with_status(status, text); status: u16, text: stringnonea new Error with that HTTP status and the message text; text is kept, like a struct field. varyk-http sends a status from 400 to 599 with its message, as the body of the response, and any other status as a 500 with a fixed body (see Errors with a status); so give a status only a message written for the client
e.message()readsthe message: part of e, not a copy (see "Returning part of a parameter")
e.status()readsthe HTTP status as an Option<u16>, a copy; None for an error made with Error::new or by any standard call. A status is set with Error::with_status or by varyk-http's functions over it, http::bad_request(text) and the others (see Errors with a status), and means the message was written for the client

{} prints an Error's message, and two errors can be compared with ==. There is no conversion into Error from another error type: a function's own error enum becomes one with map_err and a function that gives its text, r.map_err(|e| Error::new(describe(e))). In the generated Rust, Error is ::varyk_std::Error and parse is ::varyk_std::parse, from the varyk-std crate; a single file that uses either (names Error, or calls Error::new, Error::with_status, parse, or a json or env call) gets it as a dependency at exactly the compiler's version.

Option<T>:

CallUses the valueResult
o.is_some()readsbool
o.unwrap_or(d); d: TtakesT: the value inside, or d for None
o.ok_or(e)takesResult<T, E>: Ok of the value inside, or Err(e) for None; E comes from where the result goes, as for None, and otherwise from e
o.map(|x| ..); x: TtakesOption<R>, R what the closure gives: Some of the closure's value for the value inside, or None for None (see Closures)

Result<T, E>:

CallUses the valueResult
r.is_ok(), r.is_err()readsbool
r.ok()takesOption<T>: the Ok value, or None for an Err
r.unwrap_or(d); d: TtakesT: the Ok value, or d for an Err
r.map_err(|e| ..); e: EtakesResult<T, R>, R what the closure gives: the Ok value as it is, or Err of the closure's value for an Err (see Closures)

To look at the value inside an Option or Result, use match, if let, ?, or unwrap_or. There is no unwrap or expect: a value that is absent is something to handle, not a reason to stop the program.

A call that takes its value uses it up, as ? does: a value made right there is used up, a name holding its own value is given away and cannot be used again (V0305), and a stored value (a parameter, a field, an element, or another name for one) is copied out when everything inside it, a Result's error type included, is a Copy type (a number, bool, Time, or Uuid). So o.unwrap_or(0) on a parameter o: Option<i32> is fine, while r.unwrap_or(0) on a parameter r: Result<i32, string> is an error (V0304): match on it instead.

HashMap<K, V>:

CallUses the valueResult
HashMap::new()nonean empty HashMap; its types come from where it goes
m.insert(k, v)changesOption<V>: the value k had before, or None; k and v are kept
m.contains_key(k); k readreadsbool
m.len()readsusize, the number of keys
m.get(k); k readreadsOption<V>: the value of k, or None; looked into where it is made unless V is a Copy type (see below)
m.keys(), m.values()reads; m must be storeda chain of the keys, or of the values, in no fixed order (see Chains)

A HashMap has no m[k], and a for cannot go over one directly. Its order is unspecified, as in Rust. println! does not work on one; == does when its values can be compared (see Types).

Looked into where it is made. v.get(i) and m.get(k) give an Option whose value is part of the Vec or HashMap, and so does find on a chain of borrowed items (see Chains). When that value is of a Copy type (a number, bool, Time, or Uuid), the result is a plain Option holding a copy, usable anywhere: counts.get(word).unwrap_or(0). Otherwise it must be looked inside right where it is made, as the value of a match, an if let, or a while let:

if let Some(line) = lines.get(1) {
    println!("second: {}", line);
}

line is another name for an element of lines, as after let line = lines[1];: it cannot be changed, and lines cannot be changed while line is still used (V0307). Stored in a let, passed, returned, used with ?, or given any method, is_some() included, such a result is an error (V0208), and so is a pattern naming it whole. The Vec or HashMap it is called on must be stored, not made right there (V0001).

Literals

A number literal takes the type its place expects, such as the declared type of a let or a parameter, usize included. Where nothing is expected, a whole number is i32 and a fractional one is f64. Number types never convert into each other, so a count that meets a usize is declared as one: let mut i: usize = 0;.

Statements

let x = 5;              // a new name; the type is worked out from the value
let y: i64 = 5;         // the same, with the type written out
let mut count = 0;      // `mut` lets the name be given a new value later
count = count + 1;      // assignment to a `let mut` name
user.name = "Bob";      // assignment to a field of a `let mut` name
scores[0] = 10;         // assignment to an element of a `let mut` Vec
return x;               // leave the function with a value
while count < 10 { }    // repeat while the condition is true
while let Some(t) = tasks.pop() { }  // repeat while the pattern fits
for i in 0..n { }       // i takes 0, 1, ..., n - 1
for item in items { }   // every element of a Vec, in order
break;                  // leave the innermost `while`, `while let`, or `for`
continue;               // go to the next round of the innermost loop
print_user(user);       // any expression followed by `;`

A name can be declared again with a new let; the new one hides the old one from then on.

Expressions and operators

Expressions are literals, names, module paths (math::square), function calls, method calls (counter.add(2)), associated function calls (Counter::new()), field access (user.name), elements (v[i]), struct values (User { .. }, or m::User { .. } for a struct of module m), enum values, Option, Result, and Vec values, format!, parentheses, blocks, if/else, match, ? after a Result or an Option (see Passing errors on), and as after a number.

A block { ... } runs its statements, and if it ends with an expression and no ;, that expression is the block's value. A function body works the same way, so fn add(a: i32, b: i32) -> i32 { a + b } returns a + b.

if/else is an expression, so it can produce a value:

let label = if age >= 18 { "adult" } else { "child" };

Conditions must be bool. Chains are written if ... { } else if ... { } else { }. A struct value inside an if or while condition needs parentheses.

Operators:

Precedence, from tightest to loosest: - and ! first; then as; then *, /, and %; then + and -; then the comparisons; then &&; then ||. Operators of the same level group from left to right, so 10 - 3 - 2 is 5. Because as binds tighter than *, a as i32 * 2 is (a as i32) * 2. Comparisons cannot be chained: a < b < c is an error. Use parentheses when in doubt.

Closures

A closure is a small piece of code handed to a call that runs it: |x| expression, or |x| { block } when it needs statements. It exists only as the argument of a call that takes one: map on an Option, map_err on a Result, and map, filter, any, all, and find on a chain (see Chains):

let doubled = Some(4).map(|n| n * 2);
let checked = parse_age(text).map_err(|e| format!("{}: {}", field, e));

A closure has exactly one parameter, the value the call hands it (V0201 otherwise), with no type written: n is the i32 inside the Option, e the error of the Result. It has no return type either; the call's result holds whatever the closure gives, so Some(4).map(|n| n * 2) is an Option<i32>. A closure anywhere else, in a let, as an argument to a Varyk function, or returned, is an error (V0001): a closure is never a value of its own. A type written on the parameter, |x: i32|, and move before the closure are errors too (V0001).

The body is a value of its own, not a piece of the function around it: return, break, continue, and ? inside it are errors (V0001), since they would leave the closure rather than the function. A loop inside the body is the closure's own, and can use break and continue. A None, Vec::new(), Ok, or Err the closure gives takes its type from where the call's result goes, as in let found: Option<Option<i32>> = o.map(|n| None); (V0207 with nothing to take it from).

Inside the body the names of the function around it can be read, and nothing more: assigning to one, passing it to a mut parameter, or calling a changing method on it is an error (V0301, V0303), and so is keeping it anywhere that owns its value (V0304), such as push, a struct field, or the closure's own let mut given it; copy text with .clone(). A let of it inside the closure is another name for it, and it is still there after the closure. The parameter is read-only too, like a name a match pattern makes: to change it, first write let mut n = n;.

The closure of map and map_err must give something new, because the Option or Result it makes owns what it holds: a number, a new string, a call's result, the parameter itself, or a string literal, which becomes new text (o.map(|n| if n > 5 { "big" } else { "small" }) is an Option<string>). Giving a name from outside, or part of the parameter, is an error (V0304; .clone() for text), and giving parts of two different names is V0308.

In the generated Rust a closure is written as it is, and a name from outside is borrowed by it without being changed: an owned String stays a String and is passed on as &name, as anywhere else.

Chains

A chain goes over the elements of a stored value one by one, in one expression: it starts at a source, passes through any number of maps and filters, and ends at a call that gives a value.

let total = numbers.iter().sum();
let evens = numbers.iter().filter(|n| n % 2 == 0).count();
let names: Vec<string> = users.iter().map(|u| u.name.clone()).collect();

The sources are v.iter() on a Vec, s.split(sep) on a string, and m.keys() and m.values() on a HashMap, each on a stored value (or, for split, a string literal): on a value made right there it is an error (V0001), so store it with let first. The calls of a chain, I its item type:

CallResult
c.map(|x| ..); x: Ia chain of what the closure gives
c.filter(|x| ..); x: I, gives a boola chain of the items for which the closure gives true
c.collect()Vec<I>, the items in order; they must be owned or copies (see below)
c.count()usize, the number of items
c.sum(); I a number typeI, the items added up
c.any(|x| ..), c.all(|x| ..); give a boolbool: whether the closure gives true for any item, or for every one
c.find(|x| ..); gives a boolOption<I>: the first item for which the closure gives true, or None; looked into where it is made when the items are borrowed (see below)

A chain must be finished where it is written: stored in a let, passed, returned, used as a statement of its own, or put inside Some or a vec!, an unfinished chain is an error (V0208), because it has no type to write. collect needs no type written, and sum on items that are not numbers is an error (V0200). A chain can also end as the head of a for (see Loops).

Items. Every item is borrowed, a copy, or owned:

filter keeps the kind. map gives copies when its closure gives a value of a Copy type, owned items when it gives something new (a call's result, a .clone(), a format!, a string literal), and borrowed items when it gives part of its item (users.iter().map(|u| u.name)), or part of a name from outside it (|x| other.name), which the items are then parts of. Giving part of an owned item is an error (V0304): it ends with the closure. Giving something new in one place and a part in another is V0304, and parts of two different things V0308.

The closure of map gets the item itself. The closures of filter, any, all, and find only look at it, since it goes on down the chain or becomes the result: keeping it anywhere that owns its value is an error (V0304); map is where a closure makes something new from an item. All of them read names from outside as any closure does.

collect makes a Vec that owns its items, so a chain of borrowed items must copy them first, words.iter().map(|w| w.clone()).collect() (V0304 otherwise). find on copies or owned items gives a plain Option; on borrowed items the Option holds part of the stored value and is looked into where it is made, as get's is: its value is another name for that part, and the stored value cannot change while it is used (V0307).

if let Some(word) = words.iter().find(|w| w.len() > 3) {
    println!("{}", word);
}

In the generated Rust a chain is written as it is, with .copied() after a source of a Copy type, collect::<Vec<_>>(), and sum::<T>().

Matching

match looks at an enum, an Option, a Result, a number, a bool, or a string and runs the first arm whose pattern fits. Like if, it is an expression:

fn area(shape: Shape) -> f64 {
    match shape {
        Shape::Circle(r) => 3.14 * r * r,
        Shape::Rect(w, h) => w * h,
        Shape::Point => 0.0,
    }
}

fn grade(score: i32) -> string {
    match score {
        90..=100 => "A",
        80..=89 => "B",
        _ => "lower",
    }
}

Each arm is pattern => expression,; the comma may be left out after a block. An arm that starts with a block ends at its }, as in Rust, so { 1 } + 2 there is written ({ 1 }) + 2. Every arm must have the same type, or no arm has a value (a println!, a call that returns nothing, or a block without a final expression). An arm that always returns fits anywhere; a return, break, continue, or assignment as an arm's body is written as a block, Some(x) => { return x; } or Some(x) => { t = t + x; }. None, Vec::new(), Ok, and Err in an arm take the type the match is expected to have, or else the type of an earlier arm.

A pattern is one of:

Patterns nest: Some(Shape::Circle(r)), Ok(Some(x)), Event::Click { x: 0, y }. A name may appear only once in a pattern. A literal or range end must have the value's type and fit it (300 against a u8 is an error). A number with a fractional part cannot be matched with a pattern (use if), and a string literal can only be the whole pattern of a match on a string, never inside another pattern: for Some("yes"), name the value and compare it with == in the arm. A struct cannot be taken apart by a pattern (name it and look at its fields with if). Not in Varyk: guards (pattern if condition), a | b, .., and @.

Every value must fit some arm: for an enum, Option, or Result, every variant at every depth; for a bool, both values; and for a number or a string, a last arm _ => ... or a name, because Varyk counts them as having more values than any list of arms can name: 0..=255 on a u8 still needs one, which is never counted as unreachable. The error names a value no arm fits, such as Some(Shape::Point) or Event::Click { x: _, y: _ }. An arm that can never run, because the arms above it already fit everything it fits, is an error too: 4 after 1..=5, a variant repeated after an identical arm, or anything after _ or a name.

if let runs its block when a pattern fits and its else, which may be another if or if let, when it does not; like if, it is an expression, and the else may be left out when the block has no value. while let repeats its block while the pattern fits, looking at the value anew each round; break and continue work in it as in while:

if let Some(user) = users.get(0) {
    println!("{}", user.name);
} else {
    println!("nobody");
}

while let Some(task) = queue.pop() {
    run(task);
}

The value an if let or while let looks at follows every rule of a match's, below, and so do the names its pattern makes, which exist only in its block.

The names a pattern makes exist only in their arm, like a let inside a block: one that has the same name as an outer name hides it in the arm, and the outer name is back, unchanged, after the match.

Matching borrows what is stored. The value matched on is either a name, a field, or an element (something stored), or a value made right there (a call result or a new value). An if, a block, or a field or element of a value made right there (make().status) is an error: store it with let first.

None of the names a pattern makes can be changed. To change a copy, first make a changeable one: let mut n = n;; to change the stored value, change it by its own name.

So an Option<Item> kept in a let cannot give its Item away: match only looks inside it. Match on the call that made it instead: match tasks.pop() { Some(task) => done.push(task), None => {} }. When the stored value was made by a match, if, or block, move that into a function that returns it and match on a call of the function.

A match used as a value follows the rule of if: when every arm gives something stored, the result is another name for it; when every arm gives a new value, the result owns it. Mixing the two is an error for text, and wherever the value is only read in place, such as in a call or a println!; store it with let first.

In the generated Rust, a stored value is matched by reference (match &shape), a string as a &str (match name.as_str()), and a copied name is copied at the start of its arm with let n = *n;.

Loops

while repeats while its condition is true. for goes over a range of integers, the elements of a Vec, or the items of a chain (see Chains):

for i in 0..n { }        // i takes 0, 1, ..., n - 1
for i in 1..=n { }       // i takes 1, 2, ..., n
for task in tasks { }    // every element of tasks, in order
for word in text.split(" ") { }            // every piece of text
for n in numbers.iter().filter(|n| n > limit) { }

A range a..b counts from a up to, but not including, b; a..=b counts up to and including b. Both ends are integers of one type, and a written number takes the other end's type, so 0..tasks.len() counts in usize. Ranges exist only in the head of a for. Nothing else can be looped over: not a string, not an Option, not a range of anything but integers (V0200), and not a HashMap, whose rounds would need a key and a value together (V0001). break and continue work in for as in while. The loop variable exists only in the loop body, and is a new name each round.

Looping borrows what is stored, as matching does. The Vec looped over is either stored (a name, a field, or an element) or made right there (a call result or a vec!). An if, a block, or a field or element of a value made right there is an error: store it with let first.

The loop variable can never be changed. To change a copy, first make a changeable one: let mut n = n;; to change the stored Vec, change it by its own name, after the loop.

In the generated Rust, a stored Vec is looped over by reference (for x in &v), and a copied element is copied at the start of the body with let x = *x;; a chain is written as it is.

Passing errors on

r? takes the value out of a Result: when r is Ok(v), r? is v; when it is Err(e), the function returns Err(e) at once. It works only in a function that returns a Result, on a Result with the same error type:

fn load(id: i32) -> Result<User, string> {
    let name = name_of(id)?;    // name_of returns Result<string, string>
    Ok(User { name: name, age: 36 })
}

o? does the same for an Option: when o is Some(v), o? is v; when it is None, the function returns None at once. It works only in a function that returns an Option, on an Option.

Anything else is V0206: ? in a function that does not return a Result or an Option (a main that returns nothing included; main may return a Result whose error is Error instead), on an Option in a function that returns a Result or a Result in a function that returns an Option (the message names both types), on a value that is neither, or on a Result whose error type is different; an error is never converted into another type.

A Result with Error where its value is wanted, as in let id: i64 = db.one(..).await;, is a V0200 mismatch. In a function that returns a Result with Error its help inserts the ? (after the .await); in any other function a note says to take the value out with match or if let.

What a ? is expected to produce flows into its operand, so let n: i32 = Ok(x)?; and let n: i32 = Some(x)?; type without more annotation, and Ok(x)? alone takes the function's error type. Err(e)?; as a statement has no value type to take and is V0207: write return Err(e);. In the generated Rust, the type of a bare Ok, Err, or None under ? is written out (Ok::<i32, String>(x)?).

? can be used anywhere a value can: in a let, in an argument, in a match arm, in a loop, or as a statement of its own (check(text)?;). It takes what it is given, like a return does: a Result made right there is used up, a name holding one is given away (using it again is V0305), and a Result the function only borrows, such as a parameter, a field, or an element, cannot be used with ? (V0304). In the generated Rust, r? is written as it is.

Logging

The four log calls write a line to stderr. Like json and env, log is written with the module's name; there is no use log;.

CallUses the valueResult
log::debug(format, args..); args readnonenothing: a debug line
log::info(format, args..); args readnonenothing: an info line
log::warn(format, args..); args readnonenothing: a warning line
log::error(format, args..); args readnonenothing: an error line
// main.vr
fn main() {
    let port = 8080;
    log::info("listening on port {}", port);
    log::warn("queue is {} percent full", 90);
}

The format string and its arguments follow println! exactly: the text is a string literal written in quotes (anything else is V0202), the number of {} must match the number of arguments (V0202), and the arguments are numbers, bool, strings, an Error, a Time, or a Uuid (V0203 otherwise).

LOG sets the level, debug, info, warn, error, or off; it is info when unset, and a line below the level is not written. LOG and LOG_FORMAT are read as env::parse reads a variable: the process environment first, then .env. A text line, without colour, is the time, the level, and the message: 2026-09-30T12:00:00.000Z INFO listening on port 8080. With LOG_FORMAT=json each line is one JSON object with the keys time, level, and message; any other value gives text.

A program that makes a log call, uses a Varyk package that makes one, or makes an app with http::App::new (whose package logs every request and the message of every 500, see HTTP) starts logging first thing in its main and in each async test, and so needs varyk-std; a program without any of them writes nothing to stderr. Setup never stops the program: a bad LOG value or a .env that cannot be read gives one warning line, and logging goes on at info in text. In the generated Rust a call is ::varyk_std::tracing::info!(..), and main and an async test begin with ::varyk_std::start();, which does nothing when logging is already started. A library's log calls go wherever the program using it sends them.

Tests

A top-level function marked #[test] is a test, in any module, pub or not. varyk test builds the program with its tests and runs them; varyk build and varyk run leave them out. Two calls check results, and only inside a test:

CallUses the valueResult
assert(cond); cond a boolnonenothing; the test fails when cond is false
assert_eq(a, b); a and b readnonenothing; the test fails when a != b
// main.vr
fn total(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    println!("{}", total(2, 3));
}

#[test]
fn adds_two_numbers() {
    assert_eq(total(2, 3), 5);
    assert(total(0, 0) == 0);
}

A test takes no parameters, returns nothing, cannot be called from the program, and cannot be the entry file's main (V0114). assert takes a bool (V0200 otherwise). assert_eq takes two values of one type (V0200 otherwise) that == can compare (V0203 otherwise), reads them as == does, and works out each one once. Outside a test both are V0114: a check that stops the program has no place in service code, which returns an Err instead.

A failing check stops its test and names the Varyk file and line, the file from the package's root, or for a single file its name alone, wherever varyk runs: assertion failed at src/store.vr:12. assert_eq adds both values when they print with {} (numbers, bool, strings, Error, Time, and Uuid): assertion failed at src/store.vr:12: left is 4, right is 5.

varyk test shows the Rust test runner's report, which names a test in a module by its path (store::adds_two_numbers), and exits 0 when every test passes. In the generated Rust a test is a #[test] function, assert is ::std::assert!(cond, "assertion failed at .."), and assert_eq is ::std::assert! on == of its two values.

Printing

println! prints a line. The first argument is a string literal, and each {} in it is replaced by the next argument:

println!("{} is {} years old", name, age);

The number of {} must match the number of arguments. Write and to print { and }. Nothing may go inside the braces. The arguments must be numbers, bool, strings, an Error, which prints its message, a Time, which prints as to_iso writes it, or a Uuid, which prints in lowercase with hyphens; a struct, an enum, an Option, a Result, a Vec, or a HashMap cannot be printed whole, even one that can be copied and compared. Nor can a Bytes, which has no one printed form: print b.to_text() or b.to_base64() instead.

format! follows the same rules and, instead of printing, makes new text: let line = format!("{} is {}", name, age);. It is the way to join strings.

Functions and parameters

fn add(a: i32, b: i32) -> i32 {
    a + b
}

A function without -> returns nothing. A function returns the value of its body or of a return statement.

Functions borrow by default. A parameter written user: User lets the function read the value it was given, but not change it. The caller keeps the value and can use it again afterwards. You never write anything special at the call: print_user(user) twice in a row is fine.

mut means the function may change the caller's value. A parameter written mut user: User may be changed inside the function, and the caller sees the change. The value passed in must itself be changeable: a let mut name, a mut parameter, a field or element of one of those, or a fresh value such as User { name: "Alice" } or a call result. The compiler says so if it is not, and suggests where to add mut.

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

Numbers and bool are simply copied into a parameter without mut, which behaves the same as borrowing. A mut i32 parameter changes the caller's number like any other mut parameter.

Giving a value away. let b = a; gives the value of a to b, if it is a string, a Bytes, or a struct. After that, a cannot be used, and the compiler points at the line where it was given away. Numbers and bool are copied instead. The same happens when a name is stored in a struct field, put inside an enum value, Some, Ok, or Err, or in a vec!, passed to push, returned, or passed to a Rust function that takes a String or a varyk_std::Bytes.

A name made with let from a parameter, or from any field or element, is another name for that same value, not a copy; so is a let mut text name after one of these is assigned to it, and so is a name a match pattern makes for part of a stored value, or the variable of a for over a stored Vec. Changing it changes the original. While the other name is still used later, the original cannot be changed or given away; and when the other name was made with let mut and can change the original, the original cannot be used at all until the other name is no longer needed (V0307). Changing includes calling a changing method on the original or on a part of it: after let first = tasks[1];, tasks[0].complete() changes tasks. A loop body counts as coming after itself.

What a function only borrows, it cannot keep. A string or struct parameter belongs to the caller. A field that holds a string or a struct belongs to the struct around it, even when that struct is your own. The same goes for a let name made from one of these, and for an element of a Vec, which belongs to the Vec. Such a value cannot be stored into a struct field or an element, put inside an enum value, Some, Ok, Err, or a vec!, or passed to push; the error says what to do instead. For text, the fix is a copy: user.name.clone().

A Bytes is passed, stored, and moved as a string is: a parameter borrows it, a field owns it, let c = b gives it away, and a function may return a Bytes parameter or one held in a parameter. Its fix is .clone() too, which copies only the handle to the bytes, not the bytes themselves.

Returning part of a parameter. A function may return part of one of its parameters without copying it: every value it returns (its last value, and every return, each branch of an if or match counting on its own) is part of the same parameter, self included, and the function does not change that parameter.

impl User {
    fn display_name(self) -> string {
        if self.nickname.is_empty() { self.name } else { self.nickname }
    }
}

fn first(users: Vec<User>) -> User {
    users[0]
}

Nothing is written for it: Varyk works it out. The result of such a call is another name for part of the value passed in that position (the value a method is called on, for display_name), as a field is: let n = user.display_name(); cannot be changed, cannot be kept anywhere that owns it (copy it with .clone()), and user cannot be changed or given away while n is still used (V0307). It can be looked at by match, if let, while let, and for, and have calls made on it. The value passed in that position must be stored, or be a string literal: make_user().display_name() is an error (V0001), since the new User would be gone at the end of the line; store it with let first. s.trim() works the same way.

These are errors, each fixed by returning a copy (.clone() for text and Bytes) instead: returning part of a parameter in one place and something new in another, a string literal included, or beside a ?, which returns a new None or Err early (V0304); parts of two different parameters (V0308); part of a let of the function, or of a number or bool parameter or for variable, which the function gets as its own copy, since either ends when it returns (V0304); part of a mut parameter (V0304), since the caller could not even read what it passed while the result is used; and part of a parameter in a function that calls itself, directly or through other functions (V0304). A value of a Copy type is always copied, so returning one is never part of anything. Varyk decides a head of a match, if let, or while let by how it is written, before it knows which calls return parts, so a field of such a call there (match user.info().kind) is an error (V0001); store the call's result with let first.

For Rust readers: user: User becomes user: &User, mut user: User becomes user: &mut User, b: Bytes becomes b: &::varyk_std::Bytes, and the call sites get & and &mut. A function returning part of a parameter returns &str, &User, or &::varyk_std::Bytes, with one lifetime written on that parameter and the return when Rust's elision would not pick it. In Rust, mut name: T means an owned parameter that can be rebound; Varyk uses mut for the mutable borrow instead, and no Varyk-declared parameter takes ownership of a string or struct. Copy types are passed by value.

Async functions and tasks

async before fn makes an async function: one that can wait, on a timer here and on the network in later milestones, without holding up the rest of the program. It goes on a top-level function or on a method, after pub when there is one: pub async fn load(self) -> User. Parameters, self, mut, the return type, and the body follow the rules of any function.

// main.vr
struct User {
    name: string,
}

impl User {
    async fn greet(self) -> string {
        time::sleep(10).await;
        format!("hello, {}", self.name)
    }
}

async fn twice(n: i64) -> i64 {
    time::sleep(10).await;
    n * 2
}

async fn main() {
    let u = User { name: "ann" };
    println!("{}", u.greet().await);
    println!("{}", twice(21).await);
}

#[test]
async fn doubles() {
    assert_eq(twice(2).await, 4);
}

A call to an async function is followed by .await, which runs it right there and gives what it returns: twice(21).await is an i64. Its arguments are lent as any call's are, so u above can be used again. .await can be used inside if, match, for, while, and println! like any call, and f(x).await? passes an Err on.

async fn main() starts the program on a runtime that spreads async work over every core of the machine; a program that logs starts logging first thing inside it. Nothing may call an async main (V0106). An async main may return a Result as any main may. #[test] async fn is a test, run on a runtime of its own; otherwise the rules of Tests apply. Both keep the shape of any main and any test.

time is a standard module, like json: its call is written with the module's name, and there is no use time;.

CallUses the valueResult
time::sleep(ms); ms: u64noneasync, nothing: waits ms milliseconds without holding up other work

Two ways to call

A call to an async function, a method, time::sleep, or an async function or method of a .rs module included, is one of two kinds, decided by what follows it:

WrittenKindTypeMeaning
f(x).awaitawaitedTruns f here and gives its result; nothing is started
f(x)startedTask<T>starts f now, running alongside this function, and gives a task for its result
// main.vr
async fn fetch(id: i64) -> i64 {
    time::sleep(10).await;
    id * 2
}

async fn send_email(to: string) {
    // sending takes a while, and nothing needs to wait for it
    time::sleep(50).await;
}

async fn main() {
    let a = fetch(1);
    let b = fetch(2);
    send_email("ann@example.com").detach();
    println!("{}", a.await + b.await);
}

a and b run at the same time, so the program waits about 10 milliseconds for both, not 20, and prints 6. Nothing waits for send_email, so the program may end before it does.

An awaited call lends its arguments as any call does. A started call gives its arguments, the value a method is called on included, to the task, which keeps them while it runs and lends them to the function: a number is copied, a name holding an owned value is given away (using it later is V0305), and a new value, such as a call's result, a literal, or a .clone(), is handed over. The task may outlive the function that started it, so a value that function only borrows (a parameter, a field or element of one, or another name for one) cannot be given to it (V0304): give it a copy with .clone(). A started call cannot pass to a mut parameter or call a mut self method (V0309): the task would change only its own copy.

A started call may stand in four places only, so that no task is thrown away by accident:

The last two make a Vec of tasks, which is kept in a let or given to Task::all or Task::all_settled to wait for every task in it (see Tasks). The map closure's items are lent to it, so give the task a copy of anything but a number: names.iter().map(|n| send_email(n.clone())).collect() (V0304 otherwise).

Anywhere else, a statement fetch(1); and an argument show(fetch(1)) among them, it is an error (V0213), since the task would be thrown away and stopped at once; the fix is .await, or .detach().

Tasks

CallUses the valueResult
t.awaittakes tT: waits for the task and gives its result
t.detach()takes tnothing: the task runs on with nobody waiting for it
Task::all(tasks).awaittakes tasks, a Vec of tasks giving TVec<T>: waits for every task
Task::all(tasks).awaittakes tasks, a Vec of tasks giving Result<U, E>Result<Vec<U>, E>: every Ok value, or the first Err to arrive
Task::all_settled(tasks).awaittakes tasks, a Vec of tasks giving Result<U, E>Vec<Result<U, E>>: waits for every task and keeps each outcome

Task::all and Task::all_settled wait for every task in a Vec and give the results in the order of the Vec, whatever order the tasks finish in; an empty Vec gives an empty one (or Ok of one). Both are always followed by .await (V0212 otherwise).

// main.vr
async fn price(id: i64) -> Result<i64, string> {
    time::sleep(10).await;
    if id > 2 {
        return Err(format!("no price for {}", id));
    }
    Ok(id * 100)
}

async fn total(ids: Vec<i64>) -> Result<i64, string> {
    let prices = Task::all(ids.iter().map(|id| price(id)).collect()).await?;
    Ok(prices.iter().sum())
}

async fn main() {
    match total(vec![1, 2]).await {
        Ok(n) => println!("total {}", n),
        Err(e) => println!("{}", e),
    }
    let outcomes = Task::all_settled(vec![price(1), price(3)]).await;
    for outcome in outcomes {
        match outcome {
            Ok(p) => println!("ok {}", p),
            Err(e) => println!("failed: {}", e),
        }
    }
}

prints total 300, ok 100, and failed: no price for 3. An empty vec![] of tasks has no type to take (V0207), and Task cannot be written, so a list that may be empty is a collected map, as in total.

A program with an async main, an async test, a started call, a Task::all or Task::all_settled call, or a time::sleep call uses varyk-std, which holds the runtime. In the generated Rust an async function is an async fn and .await is written as it is; an async main is fn main() { ::varyk_std::run(async { .. }) }, an async test the same under #[test], and time::sleep(ms) is ::varyk_std::time::sleep(ms). A started call f(u, 3) evaluates its arguments in order and starts the task with them: match (u, 3,) { (varyk_0, varyk_1,) => ::varyk_std::Task::start(async move { f(&varyk_0, varyk_1).await }) }, each passed as its parameter takes it; a method is called by its path, User::load(&varyk_0), and a call with no arguments matches on (). A task is ::varyk_std::Task<T>, and t.detach() is written as it is. Task::all(ts).await is ::varyk_std::Task::all(ts).await, or ::varyk_std::Task::try_all(ts).await for tasks giving a Result, and Task::all_settled(ts).await is ::varyk_std::Task::all(ts).await.

Shared

A started call needs its own copy of what it is given. To give one struct to many tasks without a copy for each, share it:

CallUses the valueResult
Shared::new(value)takes value, a structShared<T>: puts the struct where many can read it
s.clone()reads sShared<T>: another handle to the same struct; the struct is not copied
// main.vr
struct Config {
    factor: i64,
    name: string,
}

impl Config {
    fn describe(self) -> string {
        format!("{} x{}", self.name, self.factor)
    }
}

async fn scale(config: Shared<Config>, n: i64) -> i64 {
    time::sleep(10).await;
    config.factor * n
}

async fn main() {
    let config = Shared::new(Config { factor: 3, name: "triple" });
    let tasks = vec![scale(config.clone(), 1), scale(config.clone(), 2)];
    let results = Task::all(tasks).await;
    println!("{} {}", config.describe(), results[0] + results[1]);
}

prints triple x3 9. Each task gets a handle of its own from config.clone(), and all of them read the one Config.

Shared alone does not make a program use varyk-std: in the generated Rust, Shared<T> is std's ::std::sync::Arc<T>, Shared::new(v) is ::std::sync::Arc::new(v), s.clone() is written as it is and copies a pointer, and fields and methods are reached through Rust's auto-deref.

Attributes

An attribute is written #[name] or #[name(value)] on the line before what it marks, or at the start of the same line. Several may stack. There are four:

AttributeGoes beforeMeaning
#[rename("key")]a struct field, or a variant that carries no datathe name used in JSON and the environment instead of the Varyk name
#[default(value)]a struct fieldthe value used when the key or variable is missing
#[skip]a struct fieldnever written or read by json or env
#[test]a top-level functiona test, run by varyk test
// main.vr
struct Config {
    #[rename("PORT")]
    #[default(8080)]
    port: u16,
    #[skip]
    cache: Option<string>,
}

#[test]
fn adds() {}

fn main() {}

Any other name, such as #[derive(Clone)] or #[serde(..)], is an error listing the four (.clone(), ==, and JSON need no derive). So is an attribute anywhere else (on a struct, an enum, an impl block, a method, a mod or use line, a variant that carries data, or a field of a variant), the same attribute twice, a missing value (#[rename]), or a value where none goes (#[skip(1)]); all V0112. A # anywhere an attribute cannot start is a syntax error, and so is #!.

#[rename] takes a non-empty string in quotes. #[default] takes one literal that fits the field: a whole number in the range of an integer field (-1 fits i32, 300 does not fit u8), a number with a fractional part for an f32 or f64 field (1.0, not 1, as anywhere else a number meets a float), a string in quotes for a string field, or true or false for a bool field. #[default] cannot go on an Option field, which is already None when missing, nor on a field of any other type. These are checked on every struct, used or not (V0209).

#[test] marks a test (see Tests). #[rename], #[default], and #[skip] act on the types a json or env call reaches (see JSON and Configuration); on any other type they change nothing.

JSON

The json module reads and writes JSON. Its two calls are written with the module's name, json::parse(..); there is no use json;.

CallUses the valueResult
json::parse(text); text: string readnoneResult<T, Error>: a T read from the JSON text, or an Err saying what is wrong; for text that is not JSON at all, or stops early, the message starts it is not JSON, as in it is not JSON (expected ident at line 1 column 2)
json::stringify(value); value: T readnonenew text: value as compact JSON
// main.vr
enum Role {
    Admin,
    #[rename("member")]
    Member,
}

struct User {
    id: u32,
    #[rename("userName")]
    user_name: string,
    role: Role,
    nickname: Option<string>,
    #[default(18)]
    age: u8,
    #[skip]
    password_hash: Option<string>,
}

fn load(body: string) -> Result<User, Error> {
    let u: User = json::parse(body)?;
    Ok(u)
}

fn main() {
    match load("{\"id\": 7, \"userName\": \"ann\", \"role\": \"member\"}") {
        Ok(u) => println!("{}", json::stringify(u)),
        Err(e) => println!("error: {}", e),
    }
}

This prints {"id":7,"userName":"ann","role":"member","nickname":null,"age":18}.

T for json::parse comes from where the result goes, as for s.parse(): a let with a written type, an argument, a return value, a field, and through ?. The head of a match and the value a method is called on do not give it one, so a result looked at with match is put in a let first: let r: Result<User, Error> = json::parse(body);. With no type to take it is V0207, which shows let u: User = json::parse(..)?;. A .rs function that reads its result the same way, with a type parameter T: serde::de::DeserializeOwned, takes T by these rules too (see "Calling Rust").

How values map:

A #[skip] field is never written and never read. A struct that json::parse reaches must give each of its skipped fields a value: a #[default], or an Option type, which is None (V0209). Once renamed, the keys of the fields that are not skipped must all be different, and so must the keys of an enum's variants (V0209). These are checked only on the types a json call reaches.

json::stringify cannot fail, so it gives a string, not a Result. Only these types can go through JSON: numbers, bool, string, Time, Uuid, Bytes; Option and Vec of such a type; HashMap<string, V> of one; a struct whose fields, apart from skipped ones, are such types; and an enum whose variants carry no data. Anything else is an error at the call that names the part in the way (V0210): an enum with a variant that carries data, a HashMap whose key is not string (a HashMap<Uuid, V> too, though it is a valid map), Error, a Rust type from a .rs module, or a type declared in another Varyk package (a package is built without knowing who uses it, so convert its types there, with a pub fn such as pub fn stop_json(stop: Stop) -> string). For data that varies by kind, use a struct with a field of a plain enum marked #[rename("type")] and an Option field for each kind's data. The check follows the fields all the way down, and a type that holds itself through a Vec is fine.

json::stringify and json::parse only read their argument, so the value can be used after the call. A .rs function that writes its argument the same way, with a &T parameter where T: serde::Serialize + ?Sized, takes any type json::stringify does, by these rules too (see "Calling Rust"). In the generated Rust they are ::varyk_std::json::stringify(&value) and ::varyk_std::json::parse::<User>(&text), and a type that a call reaches derives serde's Serialize, Deserialize, or both, through varyk-std, as the calls need; no other type gets them.

Configuration

env::parse() fills a struct from the environment. It is written with the module's name; there is no use env;.

CallUses the valueResult
env::parse()noneResult<T, Error>: a T read from the environment, or an Err naming the variable that is wrong
// main.vr
enum Mode {
    Dev,
    #[rename("live")]
    Live,
}

struct Config {
    port: u16,
    #[rename("db_url")]
    database_url: string,
    mode: Mode,
    token: Option<string>,
    #[default(30)]
    timeout_secs: u32,
}

fn load() -> Result<Config, Error> {
    let c: Config = env::parse()?;
    Ok(c)
}

fn main() {
    match load() {
        Ok(c) => println!("{} {}", c.port, c.timeout_secs),
        Err(e) => println!("error: {}", e),
    }
}

T comes from where the result goes, in the same places as for json::parse; with none it is V0207, which shows let c: Config = env::parse()?;, and a result looked at with match is put in a typed let first.

T must be a struct whose fields, apart from skipped ones, are numbers, bool, string, Time, Uuid, enums whose variants carry no data, or an Option of one of those. A nested struct, a Vec, a HashMap, or a Bytes field, or a T that is not a struct, or a type declared in another Varyk package, is an error at the call naming the part in the way (V0210); mark a field the program fills itself #[skip] and make it an Option.

Each field reads the variable named by its key, the field's name or its #[rename], in upper case: a field port reads PORT, and the field database_url above, renamed db_url, reads DB_URL (not DATABASE_URL). Two fields whose variables would be the same are V0209. For each field, in order:

  1. a variable set in the process environment wins;
  2. otherwise, the value in .env, if that file has the variable;
  3. otherwise, the field's #[default], or None for an Option, or an Err such as `PORT` is not set.

A value is read as its field's type: a number from its text, true or false for a bool, the variant's name or #[rename] for an enum, a Time and a Uuid as parse reads them, and a string as written. One that does not read is an Err naming the variable and the value (`PORT` is not a number: `abc`, `START`: `2026-10-07 12:00` is not a time like 2026-10-07T12:00:00Z), never a crash.

.env is read from the current directory, once, the first time it is needed. It is a list of KEY=value lines; blank lines and lines starting with # are skipped, and a value may be wrapped in single or double quotes. A missing file is fine; a malformed line is an Err naming the line. .env is never written into the process environment. Variable expansion (${OTHER}) and several files (.env.local) are not supported.

In the generated Rust it is ::varyk_std::env::parse::<Config>(), and the struct derives serde's Deserialize through varyk-std.

Strings

There is one string type, string. Behind it, the compiler picks either a borrowed &str or an owned String for each name, and --emit-rust shows which.

The allocation rules: a string literal that is placed into a struct field, an enum value, Some, Ok, Err, a vec!, or a Vec element, passed to push, returned from a function, put into a name that must own its text (such as a name that is later stored in a struct field), passed to a parameter marked mut string, passed to a Rust function that takes a String, or written as a branch of an if or match whose other branches make new text (if c { make() } else { "none" }) is copied once, at the line where the literal is written. format! and s.clone() make new text, which a name holding it owns, and so do to_uppercase, replace, join, and remove on a Vec<string>; a name that push_str changes owns its text too. A name holding trim(), a piece split gives, or the text a function returns part of a parameter as, borrows it as a &str; so does a chain item a map gives as part of something. split itself copies nothing: each piece is part of the text it is called on. clone is the one copy you write yourself, and in the generated Rust it is .clone(), or .to_string() when s is a &str. Passing a name to a Varyk function never copies it. The second copy the compiler makes is a string passed as a trailing value to a Rust facade function: Value::from copies its text as it hands the value over. Nothing else copies a string's text behind your back. A string that is already owned moves instead, with no copy.

Time, ids, and bytes

Time, Uuid, and Bytes are standard types, known without a declaration like Error. In the generated Rust they are ::varyk_std::Time, ::varyk_std::Uuid, and ::varyk_std::Bytes, and their associated calls are written in full (::varyk_std::Time::now()); a single file that names one or calls one gets varyk-std as a dependency at exactly the compiler's version.

Time: one point in time in UTC, with microsecond precision, from the year 0000 to the year 9999, the range RFC 3339 can write. A Copy type.

CallUses the valueResult
Time::now()nonethe current time, cut to the microsecond
Time::from_iso(text); text: string readnoneResult<Time, Error>, by the rules below
Time::from_unix(seconds); seconds: i64noneResult<Time, Error>; seconds since 1970-01-01T00:00:00Z; an Err outside the range
Time::from_unix_micros(n); n: i64noneResult<Time, Error>; microseconds since 1970-01-01T00:00:00Z; an Err outside the range
t.to_iso()readsstring, the time written as below
t.to_unix()readsi64 seconds, rounded down, so a time before 1970 gives the second it falls in
t.to_unix_micros()readsi64
t.add_seconds(n); n: i64readsResult<Time, Error>; n may be negative; an Err outside the range
t.seconds_since(u); u: Timereadsi64, t minus u in whole seconds, rounded toward zero; negative when u is later
text.parse()readsResult<Time, Error>, as Time::from_iso

iso names the familiar standard, as JavaScript's toISOString does; the form read and written is RFC 3339's form of ISO 8601. from_iso reads YYYY-MM-DD, T or t, HH:MM:SS, an optional fraction of one to nine digits, and Z, z, or an offset +HH:MM or -HH:MM. The time is converted to UTC, and digits past the sixth are cut, so a nanosecond time from Go reads. A leap second (:60), a space for the T, the compact form with no dashes or colons, week and ordinal dates, and a time outside the range after conversion are an Err whose message names the text: `2026-10-07 12:00` is not a time like 2026-10-07T12:00:00Z. to_iso writes UTC with Z, 2026-10-07T12:00:00Z, and a fraction only when the time has one, with its trailing zeros dropped: 2026-10-07T12:00:00.5Z, 2026-10-07T12:00:00.123456Z. No call of Time stops the program: every one that can leave the range gives a Result.

{} and assert_eq write a Time as to_iso does. Two times are compared with ==, !=, <, <=, >, and >=, a later time being the greater; a Vec<Time> can be sorted, earliest first, and searched with contains. A Time cannot be added to or taken from (t + u is V0200; use add_seconds and seconds_since), and it is not a HashMap key (V0101).

Uuid: a 128-bit identifier. A Copy type.

CallUses the valueResult
Uuid::new()nonea new version 7 id: time-ordered, ordered by creation within one process
Uuid::v7()nonethe same as Uuid::new()
Uuid::v4()nonea new random version 4 id
text.parse()readsResult<Uuid, Error>; only the 36-character form with hyphens, hex digits in either case

A version 7 id holds the millisecond it was made in, readable by anyone who has the id; a version 4 id holds nothing but randomness. If you publish ids and do not want their creation times known, make them with Uuid::v4().

Uuid::new(), Uuid::v7(), and Uuid::v4() stop the program, with the uuid crate's message, when the operating system cannot give random bytes, as Rust's own HashMap seeding and the start of the async runtime do; that cannot happen on macOS, Windows, or Linux once the system has booted. In a handler, varyk-http answers it with a 500 (see Safety). The clock never stops a program: a version 7 id takes its time from the reading Time::now() makes.

{} and assert_eq write a Uuid in lowercase with hyphens, 0192f0c4-7a3e-7b5c-9d1e-2f3a4b5c6d7e, however it was read. Two ids are compared with == and !=, a Vec<Uuid> searched with contains, and a Uuid is a HashMap key. Ids have no order: < on a Uuid, or sort on a Vec<Uuid>, is V0200.

Bytes: an immutable run of bytes. Not a Copy type: like a struct, a parameter borrows it, a field owns it, and let c = b moves it.

CallUses the valueResult
Bytes::from_text(text); text: string readnonethe UTF-8 bytes of text, a copy
Bytes::from_base64(text); text: string readnoneResult<Bytes, Error>; standard base64 with padding (RFC 4648 section 4), read strictly: another alphabet, missing padding, or a character outside the alphabet is an Err
b.to_text()readsResult<string, Error>, a copy; an Err when the bytes are not UTF-8
b.to_base64()readsstring, standard base64 with padding
b.len()readsusize, the number of bytes
b.is_empty()readsbool

The Err of from_base64 or to_text says what is wrong without repeating the text or the bytes, which can be a whole request body.

b.clone() gives a second Bytes that shares the first one's buffer: no byte is copied. Two Bytes are compared with == and !=, byte by byte, and a Vec<Bytes> searched with contains; assert_eq on them says only that they differ. {} on a Bytes is V0203, since bytes have no one printed form: print b.to_text() or b.to_base64(). A Bytes has no order, is not a HashMap key (V0101), and cannot be read with parse (V0200).

Not in milestone 5c

These do not exist yet; where one can be written, it is an error that names what is not supported. They are left out because no program has needed them so far (the roadmap lists what is scheduled): on a Vec, first, last, clear, extend, truncate, dedup, reverse, swap, and sort_by; on a chain, enumerate, zip, rev, take, skip, fold, min, max, and position; on a string, ends_with, to_lowercase, chars, bytes, lines, find, trim_start, trim_end, and split_once, and char as a type; on Option and Result, is_none, map on a Result, and_then, unwrap_or_else, ok_or_else, unwrap_or_default, and as_ref; on a HashMap, remove, is_empty, and clear; HashSet, BTreeMap, and VecDeque; a HashMap in a .rs signature (a facade returns a Vec or a struct); iter(), keys(), values(), split(), and trim() on a value made right there; Box; struct patterns on plain structs; tuples and tuple patterns, so no for (k, v) in map; struct field shorthand; a main that returns a Result with an error other than Error; a function returning part of two parameters, or of a mut parameter; a borrowed return from a Rust signature whose lifetimes Rust's elision would not settle; and Debug with {:?}.

Also not yet, from the Rust side and the package side: modules declared inside a .rs file; importing Rust tuple and unit structs; a .rs signature naming a type declared in Varyk; pub(crate) and pub(super); use with braces or globs; a pub use in a .rs file; settings taken from a Cargo workspace; custom target paths; reading [features]; sharing cargo's target/ between varyk build and cargo build; varyk init into an existing project; and a varyk command for cargo doc.

Also not yet, from the batteries: TOML; pretty-printed JSON; JSON for enums that carry data (write a type field); flatten, aliases, and custom formats in JSON; structured log fields (log::info("x", id = 1)), log targets, and spans; .env variable expansion and several .env files; a kind or a cause on Error, and automatic conversion into Error from other error types at ?; attributes on structs, methods, and variants with data; and #[default] on an Option, enum, or struct field.

Also not yet, from HTTP (see HTTP): a hook that wraps the handler and calls it itself; groups of routes as values; a handler reading a header or cookie by name without taking http::Request; a body read as anything but JSON or a multipart upload (XML, URL-encoded forms); headers for one client request beyond the client's defaults; adding a route or hook to an app a function was given, not one it made; a handler that is a method, a function of a .rs file, or a function of another package; serve taking an address (the address is a setting, in the package's README); TLS in the server, which is the job of the proxy in front of it; request and response bodies sent in pieces as they come; and an OpenAPI document written from the routes.

Also not yet, from time, ids, and bytes (see Time, ids, and bytes): a calendar date, a time of day, a duration type, time zones and local time, formatting a Time by a pattern, and a monotonic clock; ordering Uuids, the nil id, and other versions; indexing, slicing, or building Bytes piece by piece, and Bytes from a Vec<u8>; Bytes in env::parse; Time or Bytes as a map key; and a handler parameter bound to the raw body.

Also not yet, from facades (see A facade for a package): two type parameters, a where clause, or another bound; a u64, a struct, a Vec, or a map as a trailing value; varyk_std::Error anywhere but as the error of a returned Result or as the whole return; a Varyk function with a literal-only parameter or one taking any number of values; naming varyk_std::Value from Varyk code; pub use of a module, with braces, globs, or as, or of an item of another package.

Also not yet, from async code: race and any; timeouts; channels; changing a value shared between tasks (a Mutex); a task anywhere but where it is made, push of a task among them; Shared of anything but a struct, or in a field or a return type; async closures, and .await in a closure; async recursion; an async function returning part of a parameter; cancelling a task by hand; a runtime the program configures (thread count, current-thread); and inferred async, where the compiler works out which functions wait.

These are left out by design: closures as values (function types, and a closure in a let, a parameter, a return, or a field), move, a type on a closure's parameter, and a closure that changes a name from outside it; for_each (write a for); iterators as values of their own (a chain is finished where it is written); an Option holding part of a stored value as a value of its own (look inside get and find where they are made); indexing a HashMap; loop; guards, |, .., and @ in patterns, and let else; ranges anywhere but the head of a for; + on strings (use format!); Self; Copy structs; traits, generics, and attributes other than the four under Attributes; derives beyond Clone and PartialEq, so println! stays an error on structs, enums, Option, Result, Vec, and HashMap; a method that takes self by value, or any other way to write "give this away"; impl blocks for built-in types; a use of an enum variant; naming a crate from Varyk code (write a .rs facade instead: a Rust file of the package that wraps what the program needs from the crate in plain functions, as "Calling Rust" shows); and everything planned for later milestones, such as varyk fmt, and hover and completion in an editor (see Editors for what an editor shows today).

unwrap and expect are never added, in this or any later milestone: a call that stops the program when a value is absent defeats the purpose of a language for services, and match, if let, ?, and unwrap_or cover every use.

Names are ASCII only for now (letters, digits, and _); string text can be any Unicode.

HTTP

A service answers requests through varyk-http, the official HTTP package. It lives in its own repository, Varyk-Lang/varyk-http, and is published on crates.io. It is added as any package is, with varyk add http (see varyk add and upgrading), which lists it under the key http, so code writes http::App. Its README is the guide to everything it offers: the server's settings and their defaults, cookies, files, the client, and which release of the package goes with which varyk. This section is what the compiler does with it.

Varyk cannot pass a function around as a value, so the compiler knows this one package by name, as it knows varyk-std: when a build holds the package whose crate name is varyk-http, under whatever key, the calls that add routes and hooks to its App are the compiler's own, and varyk check checks each route against its function before anything is built.

// src/main.vr, after `varyk add http`
struct User {
    id: i64,
    name: string,
}

struct NewUser {
    name: string,
}

struct State {
    users: Vec<User>,
}

async fn get_user(id: i64, state: Shared<State>) -> Option<User> {
    for user in state.users {
        if user.id == id {
            return Some(user.clone());
        }
    }
    None
}

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 { id: state.users.len() as i64 + 1, name: user.name.clone() };
    let mut r = http::Response::json(created);
    r.set_status(201);
    Ok(r)
}

fn build_app(state: Shared<State>) -> http::App {
    let mut app = http::App::new(state.clone());
    app.get("/users/{id}", get_user);
    app.post("/users", create_user);
    app
}

The program's async fn main() makes the app and serves it:

let users = vec![User { id: 1, name: "Ada" }];
let app = build_app(Shared::new(State { users: users }));
let port: u16 = 3000;
if let Err(e) = app.serve(port).await {
    log::error("cannot serve: {}", e);
}

GET /users/1 is answered with a 200 and {"id":1,"name":"Ada"}, GET /users/2 with a 404, and GET /users/abc with a 400 saying that the path parameter id cannot be read from abc; get_user is not called for that one. POST /users with {"name": "Bo"} is answered with a 201.

The route table

CallMeaning
http::App::new(state)a new app; state is a Shared of the struct every handler may read (see Shared), given away as Shared::new gives its struct
app.get(path, f), app.post(path, f), app.put(path, f), app.patch(path, f), app.delete(path, f)a route: a request with that method whose path fits path calls the function f, its handler
app.before(f), app.before_on(prefix, f)a hook run before every request a route matches, or before those whose route's path is prefix or lies under it, on whole parts (/admin covers /admin/users, not /administrators)
app.after(f)a hook run on every response the router makes
app.serve(port).awaitanswers requests on port, a u16, until the program is stopped (ctrl-c, or the platform's SIGTERM, which lets the requests in progress finish), and logs the address it listens on once it binds; gives Result<bool, Error>, an Err when the port cannot be used
app.request(req).awaitsends one request through the app, without a port, and gives the http::Response (see Requests in a test)

f is a function's name with no parentheses: get_user, not get_user(). path and prefix are text written in quotes in the program (V0217 otherwise). Adding a route or a hook changes the app, so its name is a let mut (V0302). Each route is checked against the state's type, which http::App::new fixes, so routes and hooks are added where the app is made: to a name given http::App::new(..) in the same function, each by a statement of its own, not inside a loop (which would add the route again on every turn) or a closure, and that name, or any other name holding an app, is never given another value (V0221 for each). The app itself is an ordinary value: a function may return it, as build_app does, so that main and the tests make the same app. In build_app, state is a parameter, which the function only borrows, so it gives the app a copy of the handle, state.clone() (V0304 otherwise).

The package has more calls on App, ordinary calls of an imported struct: the address to listen on (only this computer, 127.0.0.1, unless a call says otherwise, so a service run on a laptop is not open to its network), CORS, limits, and the rest its README lists.

Routes and their handlers

A path is / followed by parts separated by /. A part is either written out, in ASCII letters, digits, -, _, ., and ~, or is a name in braces, {id}, which stands for whatever the request has there. No part is empty, no name is used twice, and only the path "/" ends with /. Anything else is V0222, and so is a second route with the same method and path. A path matches whole: /users/{id} matches /users/1, not /users/1/posts.

The handler is an async fn of the current package, in any module from which the route can see it (V0105 otherwise: admin::list_users must be pub). It is not main (V0106), a test (V0114), a method, a function of a .rs file, or a function of another package (V0220). Each of its parameters gets its value from the request by the first of these rules that fits:

  1. a parameter with the name of a {name} of the path is that part of the path; its type is an integer type, bool, string, Time, or Uuid; a Time is written as Time::from_iso reads it, a Uuid in its hyphenated form (/orders/{id} with id: Uuid);
  2. a parameter of type Shared<State>, where State is the struct given to http::App::new, is the app's state; there is at most one;
  3. a parameter of type http::Request is the request itself, to read its headers, cookies, or body (the package may add more types like it);
  4. on post, put, and patch, one parameter of a type JSON can hold, a struct, an enum, or a Vec or HashMap, is the request's body, read as JSON with the rules and attribute checks of json::parse (V0209, V0210);
  5. any other parameter of an integer type, bool, string, Time, Uuid, or an Option of one is read by its name from the query string, the part of the address after ?: search(prefix: Option<string>, exact: bool) reads /search?prefix=A&exact=true. A plain type must be there; an Option is None when it is not.

A parameter that fits none of these, a {name} with no parameter, a body on get or delete, two bodies, two states, or two http::Requests, a Shared of another struct, or a path parameter of another type (a Bytes among them, which is neither a path nor a query parameter) is V0219, one for each problem, at the route, naming the parameter or the part. So renaming a parameter cannot quietly stop it from getting its value. A query parameter's name is part of the address clients write, so renaming one changes what they must send.

The request is read before the handler runs. A part of the path or a query value that is not a value of its type (abc for an i64, 42 for a Uuid), a missing query value, or a body that is not JSON of its type is answered with a 400 whose body names the parameter and what is wrong, and the handler is not called: inside a handler, every parameter is a real value of its type. A path no route matches is answered with a 404, and a known path asked for with another method with a 405.

A handler is called as any Varyk function is: its parameters borrow what they are given, and a mut parameter (mut user: NewUser) may be changed.

Responses

What a handler returns is its answer:

ReturnsAnswer
nothing204, with no body
a value json::stringify writes, other than an Option200, with the value as JSON
Option<T>200 with T as JSON, or 404 for None
http::Responsethe response as built
Result<X, Error>, X one of the rows above but nothingX's answer for Ok, and the error's for Err (see Errors with a status)

Any other return type is V0220, and a type JSON cannot write is V0210. A string is sent as a JSON string. For another status, a header, or plain text, build an http::Response: http::Response::json(value) (which reads value, as json::stringify does), http::Response::text(s), or http::Response::empty() (a 204), then r.set_status(201) or r.set_header("location", "/users/3"). A handler that can fail and has nothing to send returns Result<http::Response, Error>, with Ok(http::Response::empty()). A handler that takes a type for a live connection, which the package may add (a WebSocket, say), answers before the handler runs and ignores what it returns, so it returns nothing, or Result<http::Response, Error> for ? to work on its calls.

Hooks

HookIts function
app.before(f), app.before_on(prefix, f)async fn f(req: http::Request) -> Result<bool, Error>, or with a second parameter state: Shared<State>
app.after(f)async fn f(req: http::Request, mut res: http::Response), returning nothing, or with a third parameter state: Shared<State>

The parameters come in this order, under any names; any other shape is V0220. A before hook decides whether the request goes on: Ok(true) lets it through, Ok(false) answers 403 with the package's forbidden body, for a check with nothing to explain, and Err(e) answers with the error. before_on runs only for routes under its prefix, which is written as a path without {name} parts (V0222) and matched on whole parts against the path of the route the request matched, as the program wrote it: "/admin" covers the routes /admin and /admin/users/{id}, not /administrators. A hook's request cannot be mut (V0220): a change to it would not reach the handler.

A request no route matches, a 404 or a 405, runs no before hook. An after hook runs on every response the router makes, that 404 or 405 and a before hook's refusal included, and changes it through its mut parameter (res.set_header(..)); without mut it only reads it, to log it, say.

Hooks run in the order they were added, and each covers every route of the app, wherever its line is: a before added after the routes still runs before each of them. A handler that needs to know who is asking calls a function of the program, let user = current_user(req, state)?;; a hook does not hand it anything.

async fn check_key(req: http::Request, state: Shared<State>) -> Result<bool, Error> {
    match req.header("x-key") {
        Some(key) => Ok(key == state.key),
        None => Err(http::unauthorized("an `x-key` header is needed")),
    }
}

async fn stamp(req: http::Request, mut res: http::Response) {
    res.set_header("x-served-by", "users");
}

These run with app.before_on("/admin", check_key); and app.after(stamp); in build_app, for a State with a key field.

Requests in a test

app.request(req).await sends one request through the app, hooks included, without a port, and gives the http::Response the server would have sent. A request is made with http::Request::new(method, path), then req.set_header(name, value) and req.set_body(text); an answer is read with r.status(), r.header(name), and r.body():

#[test]
async fn gets_a_user() {
    let users = vec![User { id: 1, name: "Ada" }];
    let app = build_app(Shared::new(State { users: users }));
    let r = app.request(http::Request::new("GET", "/users/1")).await;
    assert_eq(r.status(), 200);
    assert_eq(r.body(), "{\"id\":1,\"name\":\"Ada\"}");
}

request only reads the app, so one app may answer many requests. A program with an http::App::new call starts logging in main and in every async test, whether or not it calls log (see Logging), so the message of a 500 is on the screen under varyk test too.

Errors with a status

An Error may carry an HTTP status (e.status(), see Types). The package's functions make one, each taking the message as a string: http::bad_request(text) (400), http::unauthorized(text) (401), http::forbidden(text) (403), http::not_found(text) (404), http::conflict(text) (409), and http::error(status, text) for any other, status a u16. They are written in Varyk over Error::with_status (see Types), and only Varyk code sets a status: no official package's Rust does. A status from 400 to 599 is sent; any other is sent as a 500, and its message is logged, with the method and path.

An error with a status from 400 to 599 is sent with that status and the body {"error":"<message>"}. An error without one, made by Error::new or passed on with ? from a call such as a database query, is sent as a 500 with the fixed body {"error":"internal error"}, and its message is logged as an error, with the request's method and path. A status therefore means the message was written for the client, and no other error's message reaches one: a database message, a file path, or an address inside an error stays in the log.

Safety

For Rust readers: varyk-http is built on axum, tower-http, and reqwest, and the generated program never names any of them. For each route and hook the compiler writes one small function, varyk_route_0, varyk_route_1, and so on, in the module of the route call, which reads each parameter through the package (varyk_req.param::<i64>("id")), calls the handler, and turns its result into a response (::http::respond_option(v)); the route becomes app.get("/users/{id}", ::http::route(varyk_route_0));. --emit-rust shows them, and a rustc error inside one is reported at its route (V0900).

Calling Rust

A module can be a Rust file. mod greet; next to greet.rs:

// greet.rs
pub fn hello(name: &str) -> String {
    format!("Hello from Rust, {}!", name)
}
// main.vr
mod greet;

fn main() {
    let name = "Varyk";
    println!("{}", greet::hello(name));
}

The .rs file is copied into the build as it is. It may use Rust's standard library and, in a package, the crates its [dependencies] names; it may not declare modules of its own or include other files. This is how a package uses a crate: Varyk code never names one, so a .rs module in the package, a facade, wraps what the program needs in plain functions, structs, and enums, and Varyk imports those (examples/packages/matcher wraps regex-lite this way). varyk check reads its pub fn signatures, its pub structs and their methods, and its pub enums (not their methods yet: call those from a plain pub fn instead), and stops there; the Rust inside it is checked by rustc when you build. If rustc rejects it, the error or warning is shown exactly as rustc worded it, at your .rs file and line — it is your Rust, in your terms. This is different from a problem in the Rust Varyk itself generates: if that is ever rejected, which should not happen apart from the known limits under "Rust enums" below and the thread rule of V0901 (also below), the failure is reported as a Varyk diagnostic, V0900, at the Varyk line that produced it, carrying rustc's message and asking you to report it as a bug (--emit-rust shows the generated code the report points at). When rustc also rejected one of your .rs files, a V0900 may only follow from that error, so it says so instead and comes after it. Its top-level pub fn items can be called from Varyk when every parameter and the return type is one of these:

Rust typeIn Varyk
bool, i8 to i64, u8 to u64, usize, f32, f64the same type, passed by value
&stra borrowed string
&mut Stringa mut string
&'static str parametera string that takes only text written in the program: a string literal, escapes included, and nothing else (V0217), so no input can reach it
Vec<varyk_std::Value> as the last parameter, written by that full pathany number of values after the other arguments, none included: each a bool, string, f32, f64, i8 to i64, u8 to u32, Time, Uuid, Bytes, or an Option of one of those (V0218; write n as i64 for a u64 or usize, and give a None a type with let first). Each is read, not given away, so a name passed stays usable; a string is copied into the value, the one copy besides a literal placed into an owned slot. A Bytes is lent and not copied: the value shares its buffer, as .clone() on a Bytes does. A program needs varyk-std when its own .rs module has such a function, called or not, and when it calls one of a dependency package
String parameteran owned string: a literal is copied, an owned string moves
String returnstring
&str or &S return, where lifetime elision names the parameter it borrows from (below)a borrowed return of that argument: string, or the struct or enum
S, a struct or enum imported from a .rs file of the package (below)that struct or enum, given away (moved)
Vec<T>, Option<T>, Result<T, E> where T and E are in this tablethe same Varyk type, given away (moved)
Result<T, varyk_std::Error> return, written by that full path, where T is in this tableResult<T, Error>: ? opens it in a function returning Result<_, Error>, and match reads e.message(); a program needs varyk-std when its own .rs module has such a function, called or not, and when it calls one of a dependency package
varyk_std::Error as the whole return, written by that full path (not in a parameter, a field, or inside another type)Error, for a facade that makes an error of its own (varyk-http's own constructors are Varyk over Error::with_status); a program needs varyk-std for such a function as for the Result return above
varyk_std::Time, varyk_std::Uuid, written by that full path (a leading :: allowed), as a parameter, a return, a pub field, the field of an enum's variant, or inside Option, Vec, or ResultTime, Uuid, copied (see Time, ids, and bytes); a program needs varyk-std for such a function as for varyk_std::Error
&varyk_std::Bytes parameter, written by that full path (a leading :: allowed)a borrowed Bytes, read; a program needs varyk-std for such a function as for varyk_std::Error
varyk_std::Bytes, written by that full path (a leading :: allowed), in the same places as varyk_std::TimeBytes, given away (moved); a program needs varyk-std for such a function as for varyk_std::Error
Result<T, varyk_std::Error>, Result<Option<T>, varyk_std::Error>, or Result<Vec<T>, varyk_std::Error> return of a pub fn or method with one type parameter T: serde::de::DeserializeOwned (or varyk_std::serde::de::DeserializeOwned, which needs no serde dependency), written inline by that full path, and T nowhere elseT is the type the result is used as, found as for json::parse: a let with a written type, an argument, a return value, or a field, through ? and .await; with none, or for a started call (no .await), it is V0207. T is any type json::parse reads, with the same attribute checks (V0209), and a Rust type from a .rs module or a type of another package is V0210. The generated Rust writes T after the name, as in crate::db::Store::one::<User>(&db, ..). The result is a new value the caller owns; a program needs varyk-std for such a function as for varyk_std::Error
&T parameter of a pub fn or method with one type parameter T: serde::Serialize + ?Sized (or varyk_std::serde::Serialize + ?Sized), written inline by that full path, and T nowhere elseany type json::stringify writes, typed from the argument: a number, bool, string, Time, Uuid, Bytes, an Option, Vec, or HashMap of those, or a struct or enum of the current package, with the same attribute checks (V0209); a Rust type from a .rs module or a type of another package is V0210. The argument of an awaited or plain call is read, not given away, as json::stringify's is, so a name passed stays usable; a started call (no .await) gives it to its task, as it gives every argument (see Two ways to call). The generated Rust lends it as json::stringify does, where a string may be a &str, which is why the bound needs ?Sized (V0108 without it). Rust works out T from the argument, so nothing is written after the name. A program needs varyk-std for such a function as for varyk_std::Error
&T or &mut T where T is one of the value types above but String, Time, Uuid, and Bytesborrowed, or mut
() return, or nonenothing

Any other signature cannot be called: &String (take &str instead), generics other than the two type parameters just above (two of them, both kinds at once, a where clause, another bound, a lifetime parameter, a DeserializeOwned T in a parameter or elsewhere in the return, or a Serialize T anywhere but once as &T in one parameter; the note says which), lifetimes (&'static str anywhere but a parameter included), trait objects, HashMap and other std types, varyk_std::Value anywhere but in a last Vec<varyk_std::Value> parameter, references in the return type other than the ones just above (return an owned value such as String), () inside another type (Result<(), String>; use bool or a struct instead), varyk_std::Error anywhere but as the error of the returned Result or as the whole return (or as a bare Error a use brings in: write the full path), &varyk_std::Time and &varyk_std::Uuid (take them by value), &mut varyk_std::Bytes (take &varyk_std::Bytes or varyk_std::Bytes), a borrowed one of the three returned (return it by value), a bare Time, Uuid, or Bytes a use brings in (write the full path, as in varyk_std::Time), and unknown types. Calling such a function is an error that shows its Rust signature and what to change. unsafe fn, const fn, trait methods, names a pub use brings in, and functions, methods, structs, and enums marked pub(crate), pub(super), pub(self), or pub(in ..) are not imported: Varyk imports only plain pub. A function, method, struct, or enum behind #[cfg(..)] or #[cfg_attr(..)], or a function or method with a parameter or self behind one, may not exist in the build, so it is not imported either, and neither is a #[test] function. Calling or naming any of these is an error with a note saying why; for a pub(..) function the note says to make it plain pub. A module that uses a glob import (use ...::*) cannot expose functions with these built-in parameter or return types, because the glob could redefine any of their names; replace the glob with the names the file needs. The same goes for a top-level macro call in the file that could define names: a call of a macro_rules! of the same file (at any depth, inside an inline module too) whose text, or the text the call gives it, includes struct, enum, union, type, use, trait, fn, mod, impl, or Drop, or calls another macro; and a call of any macro not defined in the file (crate::make!(), a macro a use brings in, one of another file or crate, or any after #[macro_use] extern crate), except thread_local!. Such a call could define any name, so the error names the macro and its line; move the macro and its uses to another .rs file. A call of a macro of the file's own where none of that text has those words or calls a macro, a call of thread_local! whose text has none of those words, and a macro_rules! that is never called as an item change nothing.

A pub async fn and an async method are imported under the same rules and called as a Varyk async function is: awaited with .await, or started into a task (see Two ways to call), and only from an async function. One that returns a reference cannot be called (V0108): return an owned value instead, as an async Varyk function does.

// fetch.rs
pub async fn price(id: i64) -> i64 {
    id * 10
}
// main.vr
mod fetch;

async fn main() {
    let t = fetch::price(2);
    println!("{}", fetch::price(1).await + t.await);
}

It prints 30.

A started call may run on another thread, so everything its task is given, and everything it holds while it waits, must be able to go there. Every type Varyk declares can; a type from Rust code may not (in Rust terms, it is not Send, or not Sync: an Rc, a Cell, or a RefCell inside it). Only rustc can tell, so varyk check accepts such a program and varyk build reports V0901 at the started call, naming the Rust type: use Arc in place of Rc and Mutex or an atomic in place of Cell or RefCell in the Rust code, or await the call instead of starting it. The same holds for what a route's handler holds while it waits and for an app's state, which every request reads: V0901 is then at the route call, or at http::App::new.

A Rust type is found two ways: a bare name is an item of the same .rs file, and a full path, crate::other::Thing, is an item of another .rs file of the package. A name brought in by a use line in the .rs file is not followed (write the full path), and a .rs file cannot name a type declared in Varyk yet.

Rust structs and methods

A pub struct with named fields and no type or lifetime parameters in a .rs file is a Varyk struct, matcher::Matcher:

// matcher.rs
pub struct Matcher {
    words: Vec<String>,
    pub hits: u32,
}

impl Matcher {
    pub fn new(pattern: &str) -> Matcher {
        let words = pattern.split('|').map(|word| word.to_string()).collect();
        Matcher { words, hits: 0 }
    }

    pub fn is_match(&self, s: &str) -> bool {
        self.words.iter().any(|word| word == s)
    }

    pub fn bump(&mut self) {
        self.hits += 1;
    }
}
// main.vr
mod matcher;

fn main() {
    let mut m = matcher::Matcher::new("red|green");
    if m.is_match("red") {
        m.bump();
    }
    println!("{}", m.hits);
}

Its fields follow the struct field rules, with the Rust pub:

The pub fn items of impl Matcher blocks in the same file (any number of them) are its methods and associated functions, and Self means the struct. &self is Varyk's self, and &mut self is mut self: calling one needs a let mut. Parameters and returns follow the table. A &self method that returns &str or &S, for an imported struct or enum S (&Self too), returns part of self, whatever its other parameters, and the result is an alias of the receiver, as a Varyk function's borrowed return is: it cannot be kept in a struct or an element. A pub fn that returns &str or &S and has exactly one reference parameter, a &T and not a &mut T, returns part of that argument the same way. These are the shapes Rust's lifetime elision resolves to one parameter, so a lifetime written anywhere in the signature, such as fn pick<'a>(&self, other: &'a str) -> &'a str, is not imported. Any other borrowed return (&mut S, a &mut self method, Option<&T>, &[T], &String, a function with two reference parameters) cannot be called. A method that takes self by value or has type parameters cannot be called, and methods of trait implementations are not imported. Of a Rust struct's or enum's #[derive(..)] lists, Varyk reads Clone and PartialEq, written as bare names, and nothing else: they let .clone() and == work on the type and on the Varyk types that hold it (see Types). A hand-written impl Clone or impl PartialEq is not seen; .clone() or == on such a type is an error saying to derive the trait instead. Other derives are ignored: a Rust struct that derives Copy is still given away when passed by value.

A tuple struct, a unit struct, a struct with type or lifetime parameters, a #[repr(packed)] struct (whose fields cannot be borrowed), or a struct with no fixed size (its last field a slice, str, or dyn type) is not imported; naming one is an error that says why.

Rust enums

A pub enum with no type or lifetime parameters, whose variants are all unit or tuple variants with types from the table above, is a Varyk enum: nameable, constructible, and matchable exactly like one declared in Varyk, including exhaustiveness. A variant may hold a varyk_std::Time, varyk_std::Uuid, or varyk_std::Bytes, written by that full path, as varyk-http's WebSocket message holds Text(String) or Binary(varyk_std::Bytes); a match binds it as a Time, Uuid, or Bytes.

// kind.rs
pub enum Kind {
    Word(String),
    Number(i32),
}

pub fn classify(text: &str) -> Kind {
    match text.parse::<i32>() {
        Ok(n) => Kind::Number(n),
        Err(_) => Kind::Word(text.to_string()),
    }
}
// main.vr
mod kind;

fn describe(k: kind::Kind) -> string {
    match k {
        kind::Kind::Word(text) => text.clone(),
        kind::Kind::Number(_) => "a number",
    }
}

fn main() {
    println!("{}", describe(kind::classify("42")));
}

A variant with named fields, a variant holding a type not in the table, or a variant behind #[cfg(..)] or #[cfg_attr(..)], or with a field behind one (which may not exist in the build), makes the whole enum opaque: still a real type, so it can be held in a let, a field, a Vec, an Option, or a signature, and passed around and returned, but none of its variants can be named. Constructing or matching one is an error that says why the enum is opaque. A generic or lifetime-parameterized enum is not imported at all; naming it is an error that says why, like a tuple or unit struct. Derives are ignored.

When an enum, Rust or Varyk, has an impl Drop in one of the package's .rs files, Rust cannot move a payload out of it, so a match, if let, or while let on a call that returns one, or returns an Option, a Result, or an enum holding one at any depth, only looks inside it, as a match on a stored value does: its bindings belong to the enum and cannot be kept (V0304) or changed; copy a string one with .clone() to keep or change it. Varyk looks for the impl Drop anywhere in a .rs file, inside modules and functions too, and when it cannot tell which enum one is for (a type renamed with as or type, Drop itself renamed, re-exported with pub use, or brought in by use std::ops::*, or a macro, defined or called anywhere in the file, even as an argument of println!, whose text includes the word Drop, or any other macro call that could write one: as an item, a call that could define names by the rule above; inside a function or a const block, or as an argument of another call, a call of a macro of the file's own by the same rule, or of any macro not defined in the file other than println!, print!, eprintln!, eprint!, format!, vec!, assert!, assert_eq!, assert_ne!, debug_assert!, debug_assert_eq!, debug_assert_ne!, panic!, write!, writeln!, dbg!, matches!, todo!, unimplemented!, unreachable!, concat!, stringify!, env!, line!, file!, column!, thread_local!, cfg!, format_args!, include_str!, include_bytes!, option_env!, compile_error!, and module_path!, which count only when a call they are given counts; a path std::println! or core::println! is one of these only when nothing in the file, an item or a use, is named std or core, while ::std::println! always is) it treats every enum this way; an impl Drop written by a dependency's derive or attribute macro is not seen. Then the build stops with a V0900 carrying rustc's E0509 at the match; copy the binding with .clone() in its arm, or only read it there. The same is the one known limit for type names: a dependency's derive or attribute macro can define a type, such as a String of its own, that a signature of the file then names, and Varyk does not see it, so the build stops with rustc's error instead of check. Keep such macros out of facade files, or write the facade's signatures with primitive types only.

A Rust type named by a .rs item must be visible wherever that item is used: a Rust function or method whose signature names a type behind a module without pub can be called only where that module can be seen (V0108 elsewhere), a pub field of such a type can be read or assigned only there (V0108 elsewhere), and a variant holding one makes its enum opaque. The message names the module and says which pub mod fixes it.

A facade for a package

The shapes of the table above that name varyk_std:: are there so a package can offer, in Varyk terms, "read the result into whatever type you name" and "take these values, however many", and so a program using it writes no Rust. A facade shaped like the database package varyk-sql's, with a map in memory in place of a database:

// src/kv.rs of a package `store`, with `serde_json` and `varyk-std` in
// its [dependencies]
pub struct Store {
    entries: std::collections::BTreeMap<String, serde_json::Value>,
}

pub fn open() -> Store {
    Store { entries: std::collections::BTreeMap::new() }
}

impl Store {
    pub fn put(
        &mut self,
        key: &'static str,
        values: Vec<varyk_std::Value>,
    ) -> Result<u64, varyk_std::Error> {
        // turns `values` into JSON and stores it under `key`
    }

    pub fn one<T: varyk_std::serde::de::DeserializeOwned>(
        &self,
        key: &'static str,
        values: Vec<varyk_std::Value>,
    ) -> Result<T, varyk_std::Error> {
        // reads the JSON under `key` with `serde_json::from_value`
    }

    pub fn save<T: varyk_std::serde::Serialize + ?Sized>(
        &mut self,
        key: &'static str,
        value: &T,
    ) -> Result<u64, varyk_std::Error> {
        // stores `value` under `key` with `serde_json::to_value`
    }
}
// src/lib.vr of `store`
pub mod kv;

pub use kv::open;
pub use kv::Store;
// in a program with `store = { path = "../store" }` and `varyk-std` in
// its [dependencies]
struct User {
    id: i64,
    name: string,
}

fn add_ada() -> Result<User, Error> {
    let mut db = store::open();
    let name = "Ada";
    db.put("user/1", 1, name)?;
    let bo = User { id: 2, name: "Bo" };
    db.save("user/2", bo)?;
    db.one("user/1")
}

add_ada stores [1, "Ada"] and {"id":2,"name":"Bo"}, and reads the first back as a User. Each shape does one thing:

The pub use lines (see Files and modules) give the program the short names store::open and store::Store. The real varyk-sql has the same shapes on its Pool and Tx, async, and is added with varyk add sql (see varyk add and upgrading); its README says what it offers. For Rust readers: the call is written ::store::kv::Store::one::<User>(&db, "user/1", vec![]).

A facade whose serializer is not human-readable (a binary database format) gets a Time as its i64 Unix microseconds in a newtype struct named varyk_std::serde_names::TIME, a Uuid as its 16 raw bytes in one named varyk_std::serde_names::UUID, and a Bytes as raw bytes, so it can store each as a native type; these forms are one-way, and a human-readable serializer, JSON included, still gets the written string.

Packages

A Varyk package can use another Varyk package, and so can that package, to any depth. Until milestone 5b2 the only way in was a .rs file; now the other package is named in Varyk code like a module. With units = { path = "../units" } in [dependencies]:

use units::length::Meters;

fn total(a: Meters, b: Meters) -> Meters {
    units::length::add(a, b)
}

There is no new syntax. The one new kind of name is the name of a package.

Naming a package

A package is named by its key in [dependencies], with each - read as _ (the name the Rust crate has). route-planner = "1" is route_planner::, and sql = { package = "varyk-sql", version = "0.1" } is sql::. You choose the key, and two packages cannot share one.

The first name of a path, in an expression, a type, a pattern, or a use, is looked up in this order:

  1. a module declared in the current module, or a use alias;
  2. a standard module or type (json, Vec, ...);
  3. a package the code's own package depends on.

So a local module wins over a package of the same name. A package whose key is a standard name, a keyword, or begins with varyk_ cannot be named; rename it in Cargo.toml with package = "..", and the help of the error shows the line. After the package's name the path goes on as a crate:: path would inside that package, so units::length::add is crate::length::add there. use units; alone is an error (V0111), as a lone module name is, since the name is already in scope. A single name is never a package.

A dependency is a Varyk package when its folder has src/lib.vr; it may come from a path, a registry, or git. Any other dependency is a Rust crate, and naming one in a path or a use is V0110, whose help is the facade rule of "Calling Rust". A program (src/main.vr, no src/lib.vr) is not a package in this sense. Only [dependencies] is read: a package under [dev-dependencies] or a platform table cannot be named, and naming a dependency marked optional is V0401, because cargo may leave it out of the build.

What a package gives you

Everything the package marks pub that Rust's visibility rule lets its users reach: pub functions, structs with their pub fields, enums, methods, and associated functions, in src/lib.vr and its pub mods, and the pub items of its .rs modules. They behave as items of your own program do: parameters borrow, mut parameters and mut self change the caller's value, a function returning part of a parameter gives an alias, an async function is awaited or started, an enum is matched with every pattern and checked for exhaustiveness, and .clone() and == work where the type's fields allow.

A pub item of a library cannot name a type its users in other packages could not name, so a pub fn in src/lib.vr that returns a type of a private module is V0105 in the library itself, and so is a pub function, method, or field of one of its .rs modules that does.

What a package cannot be given from outside: an impl block (an impl names a type declared in the same file, V0001), and JSON or environment conversion (below). The #[test] functions of a package are not visible, and varyk test runs only the tests of the package it is run in.

Packages that use packages

Each package sees only the packages its own Cargo.toml lists. A value whose type is declared in a package the code's package does not list is V0115, and its help is the line to add:

let legs = route::legs();
let total = route::total(legs);

If route returns a Meters of units and your package lists route but not units, the second line is V0115. The rule is stricter than Rust needs and sound: the Rust Varyk writes sometimes spells out a type, and one rule is easier to learn than a list of places. When cargo picks two versions of one package, they are two packages: a Meters of units 0.1 from route is not the Meters of the units 0.2 your package lists, which is V0115 too, and its note names both versions.

JSON and configuration do not cross packages. A type is made readable from JSON where it is declared, and a package is compiled without knowing who will use it, so a json or env call on a type declared in another package, or holding one, is V0210. Convert it in its own package instead (pub fn stop_json(stop: Stop) -> string).

What runs when you build

When varyk builds your program, the Rust of every Varyk package in the build is exactly that package's own .rs modules plus what your compiler generates from its .vr files. Nothing else in the package is compiled or run: not its build.rs, and not generated Rust that its publisher shipped. varyk builds each package itself, from its .vr files, into a crate it writes, with the package's own Cargo.toml except that build = false and the library target is the generated root. If a module exists as both .vr and .rs in a package you depend on, the .vr file is used and the .rs file (which is what a publisher's assembly puts there) is ignored, and so is a src/lib.rs beside src/lib.vr. In the package you are building, having both is V0104, except for the root: a src/main.rs or src/lib.rs left by an earlier varyk init is ignored.

What this does not cover, on purpose: the package's own .rs modules, the crates it depends on, and their build scripts. They are Rust, visible as files or lines of Cargo.toml, and rustc's to judge, as for your own code. A Rust project that depends on a published Varyk crate with plain cargo trusts its shipped Rust as it trusts any crate.

This guarantee covers build, run, and test. It does not cover varyk publish: that runs cargo's own verification, which builds the crate the way a plain cargo user would, against the published versions of its dependencies, so a Varyk dependency's shipped Rust, and any build.rs it was published with, is compiled on the publisher's machine.

Packages and the build

When a package lists a dependency besides varyk-std, in [dependencies] or [dev-dependencies], every command that checks it (check, build, run, test, publish) first asks cargo which packages the build uses. It writes the package's manifest to target/varyk/packages/graph/, copies the package's Cargo.lock beside it (and removes an earlier copy when the package has none), and runs cargo metadata there, which may download what is not on the computer yet. The package's own folder is not written to. If cargo fails, that is V0405, with cargo's own message. A package with no other dependency never runs cargo in varyk check, and a single file has no packages.

The Varyk packages of the build are the ones reached through [dependencies] from the program, and from the Varyk packages so reached. Two things are refused, each V0401 at the program's Cargo.toml, naming the package: a Varyk package reached any other way (through [dev-dependencies], as an optional dependency a feature turned on, or through a Rust crate, even when it is also used the supported way), since cargo would compile it on its own, outside what "What runs when you build" promises; and two packages of the build with one name and version when one is a Varyk package and the other comes from a path or is a Varyk package too.

Each Varyk package of the build is checked as a program of its own, the packages it depends on first, with its own Cargo.toml, and only then the code that uses it, once per package cargo resolved, however many paths reach it. Its pub items then enter your program as items from another package. If it does not pass, its errors are shown at its own files with the note "in the package units 0.1.0, which this build uses", and checking stops; the usual cause is a package written for another version of Varyk. When its varyk-std line names a varyk newer than the compiler, one of the errors is V0404 on that line; when it names an older minor, each error gets one more note, "units was written for varyk 0.8; a newer release of it may build with this compiler". The Cargo.lock of a package you depend on is ignored: only the lock of the package being built decides versions. Every Varyk crate of the build uses the compiler's own varyk-std, whatever version a package's line names (see varyk add and upgrading). Only varyk publish asks that the varyk-std cargo picks for the build, from the lines and the lock as written, be no older than the compiler's (V0404 at the program's varyk-std line, or its Cargo.toml when it has none), since the crate it ships is built by plain cargo from those. A program counts as logging, and so as using varyk-std, when any Varyk package of the build calls log, so a package's log lines are not lost in a program that writes none.

varyk build, varyk run, and varyk test then write a crate for each Varyk package of the build under target/varyk/packages/<name>-<version>/: the package's isolated manifest and the Rust generated from its .vr files plus its own .rs modules, and nothing else from its folder. In every manifest written for the build, a dependency on a Varyk package becomes a path to that crate, under the same key, keeping package, features, and default-features. The build uses the lock cargo left when it read the graph. Files are written only when they change, so a second build with no change compiles nothing, and a change to a package reaches every package that uses it. A rustc error inside a package's crate is V0900 naming the package, with rustc's own message and no Varyk line; when it is in one of the package's own .rs modules, the note says it is that package's Rust, not a bug in Varyk. Warnings from a package's crate are not shown.

A package published with varyk publish has a different Cargo.toml from a source package: it sets build = false and its target is the generated src/lib.rs, and cargo publish adds keys of its own (autolib, autobins, readme = false, and others). In a dependency, those are accepted when each says what varyk publish and cargo write; anything else is the error it is elsewhere, with a note, for a package that does not come by path, that it may have been published with a newer cargo. Varyk reads these keys and never follows them. In a published crate a dependency on a Varyk package keeps its version (with its path made absolute), so that dependency needs a version (varyk add --path writes one), and that package is published first, as with any Rust crate.

In the generated Rust

For Rust readers: units::length::add(a, b) is written ::units::length::add(&a, &b), and a type as ::units::length::Meters. The crate name is the dependency key with - as _, and the leading :: keeps a local module of the same name out of the way. Inside a package its own items are crate:: paths, since the package is its own crate. In your crate a type of a package is written with the key under which your package lists the package that declares it, not the key of a package it came through: ::u::length::Meters when you list u = { package = "units" }.

Not yet

A Varyk package under [dev-dependencies], marked optional, or reached through a Rust crate; reading a package's [features]; skipping the compile of a package you trust; a summary file so check need not read a package's sources; rustc errors in a package mapped back to its .vr lines; and using a Varyk package from source in a plain Rust project (a published one works, as an ordinary crate).

The command line

varyk check [file.vr]                            check for errors
varyk build [file.vr] [--release] [--emit-rust]  generate and build; print the executable's path
varyk run [file.vr] [--release] [-- args...]     build, then run with the given arguments
varyk test [file.vr]                             build the tests and run them
varyk init [dir] [--lib]                         write a new package
varyk add [cargo add args]                       run cargo add in the package
varyk add http sql mongo                         add the official packages as `http`, `sql`, and `mongo`
varyk add sql [cargo add args]                   add one official package under its short name (`http`, `sql`, `mongo`)
varyk publish [--assemble-only] [-- cargo args]  check, assemble a plain Rust crate, and run
                                                  cargo publish there
varyk lsp                                        the language server your editor runs (see Editors)

Without a file, each command works on the package found by looking for Cargo.toml in the current directory and then upward; with no package there it stops with "no Varyk package here; name a .vr file (as in varyk run main.vr) or run inside a package; varyk init makes one" (varyk publish, which takes no file, says to run it inside a package), and when the Cargo.toml it finds has neither src/main.vr nor src/lib.vr, it stops with "no Varyk package here", naming that Cargo.toml and what is missing. varyk run on a library stops with "this package is a library; it has nothing to run", and varyk build on a library builds it and prints nothing.

run exits with the program's exit code (1 when main gives an Err, see Files and modules), and test with the test runner's (see Tests); a Rust error while building the tests is reported as under build. For a single file, the generated Rust project lives in the build directory, target/varyk/ under the current directory (so in your source tree if you run varyk from the .vr file's directory). For a package it is target/varyk/<name>/ under the package's directory, with cargo's build output in target/varyk/cache/ beside it, and the program is named after the package. An error or warning rustc reports in a .rs module you wrote is shown at your file and line, unchanged; a rustc error in the code Varyk itself generated, which should not happen, is reported as V0900 at the Varyk line responsible (see "Calling Rust").

varyk init and the target table

varyk init [dir] writes a new package in dir (the current directory if you leave it out), named after that directory: four files, Cargo.toml, .gitignore (/target and .env, so a local secrets file is never committed), .dockerignore (target and .env, so neither is copied into a container build), and src/main.vr (a hello-world program). The Cargo.toml names the package's root, [[bin]] with name the package's name and path = "src/main.vr", and lists varyk-std = "X.Y.Z" under [dependencies], the compiler's own version, since a program that uses Error or another varyk-std feature needs it. varyk init --lib writes src/lib.vr (one pub fn) and [lib] with path = "src/lib.vr" instead, and prints one line, "created the package <name> in <dir>; run varyk run" (varyk build for a library, and "cd <dir>, then" first when you gave a directory other than ., in single quotes if it has a space or another character a shell treats specially). The name is the directory's name made a valid crate name (my app becomes my_app). It refuses to run, and writes nothing, if the other kind's root file exists there (src/main.vr for --lib, src/lib.vr otherwise), since a package has only one, or else if any of these four files already exists there, listing them; when the directory is already a Varyk package, it says so instead.

A Varyk package is built by varyk and only by varyk: varyk build, varyk run, varyk test, and varyk publish. It has no build.rs and no stub src/main.rs; plain cargo build of a source package is not supported, since the .vr root is not Rust. The target table is required (V0406 without it): it is the one thing cargo needs to read the package (for varyk add, say), and it names the .vr root. A build.rs or a src/main.rs left from an earlier varyk init is never read by any crate Varyk builds. The Rust code Varyk generates carries #[allow(warnings, arithmetic_overflow, unconditional_panic)] on every item, the only lint setting in force, besides #[allow(non_snake_case)] on the mod line of a .vr module whose name is not in snake case, which Rust applies to that module and the modules inside it. A rustc error in the Rust Varyk generated is reported at the Varyk line (V0900).

A package that uses other Varyk packages is built as described under Packages.

varyk add and upgrading

varyk add [cargo add args] runs cargo add with exactly the arguments you give, in the package found upward from the current directory (so a relative --path is relative to the package), and passes cargo's output and exit code through; Varyk interprets none of the arguments except the official names http, sql, and mongo (below). Outside a package it says "no Varyk package here". Cargo reads the package's Cargo.toml, which names the .vr root, so varyk add --path ../lib works in any package varyk init made.

varyk add http, varyk add sql, and varyk add mongo add the official packages: each runs cargo add varyk-http --rename http, cargo add varyk-sql --rename sql, or cargo add varyk-mongo --rename mongo, so code writes http::App, sql::connect, or mongo::connect. The crate name written in full (varyk add varyk-http) does the same. Name several and each runs its own cargo add, in order, stopping at the first failure (varyk add http sql); that form takes no other argument, and varyk add http sql --features postgres is refused with "pass other arguments with one package at a time". With one official name first, any later arguments go to cargo as written (varyk add sql --features postgres). A short name with --rename is refused, "write the full name: varyk add varyk-http --rename web" (naming the package asked for), since http, sql, and mongo are also names of unrelated crates; the full name with --rename passes through to cargo, as does any call whose first argument is not an official name (varyk add serde). Each package lives in its own repository, and its README says what it offers. A path starting http::, sql::, or mongo:: that names nothing, when the package has no dependency, module, or use by that name, gets a note saying so: "sql is not a package of this build; varyk add sql adds varyk-sql".

A program that uses varyk-std (it names Error, calls one of its features, such as json, log, time::sleep, or Task::all, starts a call, or has an async main or an async test; Shared alone, which is std's Arc, does not count) needs varyk-std in [dependencies], as "X.Y" or "X.Y.Z" (a leading ^ is fine) or a table with such a version and no path, git, optional, or package (~, =, >=, * and lists are refused). The version is the oldest varyk the program or package was written for: this compiler and every later one accept it, and an earlier one refuses it (V0404, "this package needs varyk 0.8 or later", with the compiler's own version in a note). To upgrade, run cargo install varyk.

Whatever the line says, the build uses the compiler's own varyk-std: in every manifest it writes for the build, the program's and each Varyk package's, the varyk-std line becomes the compiler's version. The program's own Cargo.toml and Cargo.lock are never written. Until the program's line is on the compiler's minor and its lock holds the compiler's varyk-std, each build asks the crates.io index for it again, which needs the network every time. To end that, move the line (or add one, if the program has none) and run cargo update -p varyk-std in the program's folder; when the lock already holds two varyk-stds (a package of the build written for an older minor), cargo calls that name ambiguous, and cargo update -p varyk-std@X.Y.Z, naming the one on the compiler's minor, does it instead.

varyk publish asks more, since the crate it ships is built by plain cargo with the varyk-std its Cargo.toml and lock name, and no varyk rewrites them: the line must be on the compiler's minor ("varyk-std must be version 0.9, the version of this compiler"); a Cargo.lock that locks varyk-std on that minor must lock one no older than the compiler; and, when the program or a Varyk package it uses needs varyk-std, the one varyk-std cargo picks for the whole build must be no older than the compiler's either (V0404 at the program's varyk-std line, or its Cargo.toml). The note names both causes: a Varyk package of the build written for an older varyk, for which a release of it written for this one is the fix, or a lock that is out of date, for which cargo update -p varyk-std is. A single file has no Cargo.toml; its generated crate depends on the compiler's exact version.

varyk publish

varyk publish [-- cargo args] always works on the package found upward from the current directory; there is no single-file form. It runs the full check, assembles a plain Rust crate at target/varyk/package/<name>/ under the package, and runs cargo publish there, forwarding every argument after -- untouched (--dry-run, --allow-dirty, --token, and the rest are cargo's; Varyk interprets none of them) along with cargo's exit code and its output. varyk publish --assemble-only stops after the assembly and prints the crate's directory, running no cargo and publishing nothing.

cargo publish verifies the crate by building it as a plain cargo user would, against the published versions of its dependencies. That build compiles the shipped Rust of a Varyk dependency, and any build.rs it was published with, which varyk build never does (see What runs when you build). Use --no-verify after -- to skip it.

The assembled crate holds the generated tree of varyk build (so src/main.rs/src/lib.rs is the real generated root and every module sits at its place), every .vr source alongside the file it produced, for readers, the manifest with [package] build = false, its target the generated root, and its paths made absolute (a dependency on a Varyk package keeps its version, which varyk build drops, since cargo keeps the version and drops the path when it publishes), and the files the manifest's readme and license-file name, plus any README*/LICENSE* at the package root. It has no build.rs, so it needs no varyk: a consumer adds it to [dependencies] like any crate and builds it with plain cargo build, and cargo install of a published Varyk binary works the same way. A Varyk package that depends on a published Varyk library names it directly (see Packages).

Editors

varyk lsp is the language server: an editor starts it and talks to it through its standard input and output, in the Language Server Protocol, and you never run it by hand. It takes no arguments and ignores --message-format. The Varyk extensions for VS Code and Zed, in varyk-editors, start it for you; any other editor that speaks the protocol can too.

Each time you open or save a .vr file, the server checks the program the file belongs to, exactly as varyk check would, and the editor underlines every error at its place, with its message, its notes, and its code. It checks what is on disk, so an error you fix shows as fixed once you save. An error with a suggested fix offers it as a quick fix, which may change another file (a missing pub is added where the item is declared); the fix is offered only while the file is as the last check read it, so after an edit, save first.

The program a file belongs to is the package around the file, when its Cargo.toml has src/main.vr or src/lib.vr; otherwise the nearest main.vr in the file's folder or a folder above it, up to the folder open in the editor; otherwise the file alone. A module no mod line reaches yet gets no errors until one does.

Hover, go-to-definition, completion, and checking as you type are not there yet.

Error codes

Every error has a code. A code is never reused for a different meaning.

CodeMeaning
V0001a construct Varyk does not support yet; the message names it. Also an if, a block, or a field or element of a value made right there as the value a match, if let, or while let looks at or a for goes over, and a pattern taking a struct apart; a for over a HashMap (use keys() or values()); a get looked into on a value made right there (store it with let first), and so the value trim is called on, or the argument a function returns part of, made right there; a field of a call as such a head; a closure anywhere but as the argument of a call that takes one (the message names those calls), a type on a closure's parameter or move, and return, break, continue, or ? inside a closure; iter(), split(), keys(), or values() on a value made right there (store it with let first); a pub use of a module, of an item of another package, or with as
V0002unexpected token or malformed syntax
V0003a bad escape in a string, or a string with no closing "
V0010&x or &mut x written at a call; Varyk works out references itself
V0011&T or &mut T written in a parameter type or a return type; write name: T or mut name: T, and -> T (-> string for -> &str)
V0012lifetime syntax such as <'a> or &'a T; lifetimes are worked out by the compiler
V0100unknown name, or a variant, method, or associated function the type does not have; for Vec, string, Option, Result, HashMap, and a chain the message lists their calls; for an imported struct, a note says when the .rs file has the method but Varyk does not import it (a trait method, unsafe, const, behind #[cfg], or pub(crate) or another pub(..)), and likewise for a function or pub use name of a .rs module and for any method of an imported enum, which Varyk does not import yet; a path into an inline mod of a .rs file, whose items Varyk does not read, says so; also naming a variant of an opaque imported enum, saying why it is opaque; .clone() on a number, bool, Time, or Uuid, which is copied on use
V0101unknown type, or Option, Result, Vec, or HashMap with the wrong number of types, a HashMap key type that is not an integer type, bool, string, or Uuid (a Time or Bytes among them), or a Rust struct or enum Varyk does not import (a tuple or unit struct, one with type or lifetime parameters, a #[repr(packed)] struct or one with no fixed size, one behind #[cfg], or one marked pub(crate) or another pub(..)), or a type a pub use of the .rs file brings in, saying why
V0102unknown field, of a struct or of a variant with named fields, in a value or a pattern
V0103a name defined more than once (a method included, a use or pub use name the module already declares or brings in, a field of a variant, a field named twice in a value or a pattern, or a name twice in one pattern), or a reserved or built-in type name used as a name, or a binding named after a unit variant of its own enum (Point where Shape::Point is meant)
V0104a module file that is missing, present as both .vr and .rs (in a Varyk package this build uses, the .vr is loaded and the .rs ignored instead) or as both shop.vr and shop/mod.vr, unreadable, a .rs file that cannot be parsed as Rust, or named main or lib (or bin in the entry file), in any capitalization; a .rs file that uses a crate not in [dependencies] (or only in [dev-dependencies], or any crate in a single file) in a use or extern crate item (a crate named only in a path, other::f(), is rustc's to report, at build), declares a module of its own, or uses include!, shown at that line of the .rs file
V0105an item, method, or associated function used from outside its module without pub; a path through a module declared without pub; a pub item or field naming a type some of its users cannot see (in a library, the packages that use it included: a pub item in pub modules, or a pub function, method, or field of a .rs module they can reach, naming a type in a private module); a private struct field read, assigned, or named in a literal from outside its module; a literal of a Rust struct with a field Varyk cannot see or use; a Rust function marked pub(crate) (or another pub(...)) rather than plain pub; a pub use of an item without pub, or of one in a module that is not pub all the way from the root; a function named as a route's handler or a hook that is not visible from the route or hook call
V0106a missing fn main(), or a main with parameters or returning anything but nothing or a Result whose error is Error, main defined in a library's src/lib.vr, a call to an async main or to a main that returns a Result, or main named as a route's handler or a hook
V0107String or str written where string is meant
V0108a Rust function or method whose signature Varyk cannot call, including one naming a type its callers cannot see; the message shows the signature and what to change, for a generic one which part is outside the two type parameter shapes of "Calling Rust" (a Serialize bound without ?Sized says to add it). Also a pub field of a Rust struct whose Rust type Varyk cannot use (or cannot see), read or assigned, a Rust type reached through a use line in the .rs file rather than its full path (for Time, Uuid, and Bytes the note gives varyk_std::Time and the like), a &varyk_std::Time, &varyk_std::Uuid, or &mut varyk_std::Bytes parameter or a borrowed one of the three returned (the note gives the accepted form), and a type in a .rs file with a glob use or a macro that could define names
V0109a struct or enum that contains itself, directly or through other structs, enums, Option, or Result; a Vec or HashMap breaks the cycle
V0110a use naming a crate this compiler recognizes by name (std, core, alloc), or a path or a use starting at a dependency in [dependencies] that is a Rust crate and not a Varyk package; call a crate from a .rs module in the package instead
V0111a path Varyk cannot follow: super in the entry file, a use ending at an enum variant or at a type's method or associated function, a use whose leading name, or whose only name (use shop;), is a module declared elsewhere in the package (write it from crate:: or super::), or a use whose leading name another use made; a use of a package's name alone (use units;), which can already be used; when its leading name is also a dependency that a module or a standard name hides, a note gives the Cargo.toml line that renames the dependency (as V0100 and V0113 do)
V0112an attribute Varyk does not have (the message lists the four; derive gets a note that .clone(), ==, and JSON need none), one in a place it cannot go (the note says where it goes), the same attribute twice on one item, or a value missing (#[rename], #[default]) or not expected (#[skip(1)], #[test(1)])
V0113the name Error, the standard error type, Task or Shared, the standard types of async code, or Time, Uuid, or Bytes, the standard types for a point in time, an id, and a run of bytes, given to a struct, an enum, a module, or a use, or to a pub struct or enum of a .rs module; json, env, log, or time, the standard modules, given to a module, a struct, an enum, or a use, or a use of one (use json;, use json::parse;); assert or assert_eq given to a function or a use; a function, method, struct, enum, module, or use name starting with varyk_, kept for what Varyk adds to the Rust it writes
V0114a #[test] function with parameters or a return type, a call to or use of one, one named as a route's handler or a hook, or main of the entry file marked #[test]; assert or assert_eq outside a #[test] function
V0115a value whose type is, or holds, a struct or enum declared in a Varyk package this package does not list in [dependencies], or in another version of one it does (two versions are two packages; the note names both); the note gives the line for Cargo.toml
V0200type mismatch, including + on strings (use format!), as on something that is not a number, another number type meeting a usize, indexing something that is not a Vec, match arms of different types, a for over something that is not a Vec or a range, and a range whose ends are not integers of one type; <, <=, >, or >= on anything but numbers and Times (a Uuid or Bytes among them), and arithmetic on a Time; sort on floats, Uuids, Bytes, or structs, contains on a Vec of structs, join on a Vec of anything but strings, and parse into anything but a number, bool, Time, or Uuid (Bytes among them); sum on a chain of items that are not numbers, and a closure of filter, any, all, or find that does not give a bool; Task::all or Task::all_settled given anything but a Vec of tasks, and Task::all_settled on tasks that do not give a Result; a Shared given where the struct it holds is expected; an argument of http::App::new that is not a Shared; a Result with Error where its value is wanted, with a help that adds ? in a function that returns a Result with Error
V0201wrong number of arguments, or of values in an enum value; a variant value with named fields that leaves one out, or with the wrong kind of brackets; a closure with more or fewer than one parameter
V0202println!, format!, or a log call with the wrong number of {}, or something other than {} in braces; a log call whose text is not a string literal written in quotes
V0203{} used on anything but a number, bool, string, Error, Time, or Uuid (a Bytes among them, whose note names to_text and to_base64); ==, !=, or .clone() on a type that cannot be compared or copied, naming the field in the way and, for a Rust type, saying to derive the trait in its .rs file; == or != on a Shared, or on a type holding one
V0204a match that does not handle every value: a variant at any depth, a bool value, or, on a number or a string, the catch-all it always needs; the message names a value shape it misses
V0205a pattern that does not fit the value: a variant of another type, the wrong number of positions in a variant, a variant pattern leaving out a named field, a literal or range of another type or not fitting it, a range whose ends are reversed, a float literal, or a string literal inside another pattern; an arm that can never run, such as one after _ or a name; or a match, if let, or while let on something that is not an enum, Option, Result, number, bool, or string
V0206? in a function that does not return a Result or an Option, on a Result in a function returning an Option or the reverse, or on a value that is not a Result with the function's error type
V0207a None, Vec::new(), HashMap::new(), empty vec![], Ok, Err, parse(), json::parse(..), env::parse(), or a call of a .rs function whose result type has a DeserializeOwned type parameter whose type cannot be worked out where it is written (a started call of one included), Err(e)?;, text.parse().ok(), and a closure giving one with nothing to take its type from included; write the type in a let
V0208a value that must be used where it is made: an Option from get, or from find on a chain of borrowed items, holding part of a stored value, stored in a let, passed, returned, used with ?, given any method, or named whole by a pattern (look inside it with match or if let); an unfinished chain anywhere but as the value the next call of the chain is made on or the head of a for (finish the chain there)
V0209a #[rename] value that is not a string in quotes, or is empty; a #[default] value that does not fit its field's type ("x" on an i32, 300 on a u8, 1 on an f64); #[default] on an Option field or on a field that is not a number, string, or bool; on a type a json call reaches, a skipped field with no #[default] that is not an Option when the type is read, or two fields that are not skipped, or two variants, with the same key once renamed, and, on a type env::parse reaches, two fields whose upper-cased keys are the same variable
V0210a type that cannot go through JSON at a json::parse or json::stringify call, a call of a .rs function with a DeserializeOwned type parameter, the argument of a .rs function's &T parameter with T: Serialize, or a route whose handler's body or return type is one, or be read from the environment at an env::parse call (a T that is not a struct, or a field that is a struct, Vec, HashMap, or Bytes): an enum with a variant that carries data, a HashMap whose key is not string (a Uuid key included), Error, a Rust type from a .rs module, a type declared in another Varyk package (convert it in that package), a Shared, or a Result, anywhere inside it apart from skipped fields; the message names the part in the way
V0211a call to an async function, or .await, in a function that is not async; .await inside a closure
V0212.await after something that is not a call to an async function, Task::all, Task::all_settled, or a name holding a task; Task::all or Task::all_settled without .await
V0213a started call anywhere but a let with a name, the value .detach() is called on, a vec! element, or the value of a collected map's closure, let _ = included; a name holding a task, or a Vec of tasks, that nothing awaits, detaches, or gives to Task::all or Task::all_settled: its task would be thrown away
V0214async functions that call each other in a cycle, or an async function that calls itself, whether the calls are awaited or started, or adds one as a route's handler or hook; the message names the cycle
V0215a name holding a task used other than by .await or .detach(), or a Vec of tasks used other than by Task::all or Task::all_settled (indexed, given push, looped over, passed, returned, given to another name, or followed by .await); Task written as a type
V0216Shared of anything but a struct, at Shared::new or written, or Shared written anywhere but a parameter's or a let's type (a field, a return type, or inside another type)
V0217an argument to a .rs parameter of type &'static str, which takes only text written in the program, that is not a string literal (a name, a parameter, or a format!; pass the values after the text instead); a route's path or a before_on prefix that is not a string literal
V0218a value passed after the other arguments to a .rs function whose last parameter is Vec<varyk_std::Value>, of a type that cannot be one: anything but bool, string, f32, f64, i8 to i64, u8 to u32, Time, Uuid, Bytes, or an Option of one of those (a struct, a Vec, a HashMap, a u64, or a usize, for which the help writes as i64)
V0219a route whose path and handler do not fit, one diagnostic per problem at the route call: a {name} of the path with no parameter, a parameter that gets no value from the request (an http::Response, http::App, or http::Client among them), a body on get or delete, two bodies, two states, two parameters of one type the package gives (http::Request), a Shared of another type than the app's state, or a path parameter that is not an integer, bool, string, Time, or Uuid (a Bytes is neither a path nor a query parameter)
V0220a route's handler or a hook of the wrong shape: not an async function, a method or associated function, a Rust function, a function of another package, a closure, or a local; a handler's return type outside the list (nothing, a type JSON can hold, an Option of one, http::Response, or a Result of one of those with Error); or a hook whose parameters are not, in order, the request (http::Request, not mut), for after the response (http::Response), then optionally the app's state as a Shared, or whose return is not Result<bool, Error> for before and before_on and nothing for after
V0221a route or hook call on an app that is not a local bound to App::new in the same function (a parameter, a field, another local), inside a loop, a loop's condition or while let value, or a closure, or used as a value rather than a statement of its own; or an assignment to any name holding an app
V0222a route path that is not valid (one not starting with /, an empty segment, a trailing / other than "/" itself, a character other than ASCII letters, digits, -, _, ., and ~ in a literal segment, a {name} that is not an identifier, or a name twice), a before_on prefix that is not such a path or holds a {name}, or a method and path the app already has a route for
V0300changing a parameter that was declared without mut, by assigning to it or calling push or pop on it
V0301changing a let name that was declared without mut, or a name a match pattern or a for made, by assigning to it or calling push or pop on it; also changing, inside a closure, a name from outside it or the closure's parameter
V0302a let name without mut passed to a mut parameter or used to call a mut self method or to add a route or a hook
V0303a parameter without mut, or a name a match pattern or a for made, passed to a mut parameter or used to call a mut self method; also, inside a closure, a name from outside it or the closure's parameter so passed
V0304a value the function only borrows, stored in a struct, an element, an enum value, Some, Ok, Err, or a vec!, passed to push, used with ?, or returned; a stored Option or Result with more than numbers and bools inside, used up by unwrap_or, ok_or, or ok; returns that mix part of a parameter with something new, or that are part of a let of the function, of a number or bool parameter or for variable, of a mut parameter, or of a parameter of a function that calls itself; also a binding of a match on an enum that runs code when it is thrown away (an impl Drop), kept or given away; a name from outside a closure kept inside it, or given by a closure of map or map_err, as is part of its parameter; collect on a chain of borrowed items (copy them with .map(|w| w.clone())); the item a closure of filter, any, all, or find looks at, kept or given away; a chain's map closure giving part of an owned item, or something new beside a part; a value the function only borrows given to a started call, whose task keeps it, or a task detached inside a closure; for text and Bytes the fix is .clone()
V0305a value used after it was given away, to a started call's task among others, or a task awaited or detached twice, or a Vec of tasks given to Task::all or Task::all_settled twice
V0306a later argument changes or gives away a value that an earlier argument of the same call still borrows, or uses the value a method is called on while the method may change it, as in v.push(v.len()), or an index changes the Vec it indexes, as in v[g(v)] with g taking mut v
V0307a value changed or given away while another name for part of it is still used later (a name bound inside a looked-into get or find, or the result of a call returning part of it, included), or inside a for that goes over it or whose head reads it (the argument of split, or a name a closure of a chain in the head reads)
V0308a function returning part of one parameter in one place and part of another in another, or a closure giving parts of two names (a chain's map closure giving part of its item and part of a name from outside included); return a copy in one of them (in both, for a chain's map), or make two functions
V0309a started call passing a value to a mut parameter, or calling a mut self method: the task would change only its own copy
V0310changing something reached through a Shared: assigning to it, passing it to a mut parameter, or calling a mut self method or a changing call such as push on it
V0311an async function that returns part of a parameter; return a copy instead
V0400a package whose Cargo.toml does not say edition = "2024"
V0401something in Cargo.toml Varyk does not support yet: a key outside the fixed set check reads, a src/bin/, examples/, tests/, or benches/ directory or the root file of the other kind (src/lib.rs beside src/main.vr), which cargo would build as further targets, a setting taken from a Cargo workspace (workspace = true), dependencies for only some platforms ([target.'cfg(..)'.dependencies]), links, which needs a build script no crate Varyk builds runs, a build key other than build = false, a dependency keyed std, core, or alloc, [lints], or [patch] or [replace], in the package or in the root manifest of an enclosing workspace; or a Varyk package the build reaches other than through the [dependencies] of the program or of a Varyk package it uses (through [dev-dependencies], as an optional dependency a feature turned on, or through a Rust crate), or a Varyk package of the build with the same name and version as another path package or Varyk package of the build; the message names the package and what reaches it; also a dependency marked optional named in Varyk code
V0402a Cargo target table not supported yet: [[example]], [[test]], [[bench]], or a [[bin]] or [lib] that is not the one naming the package's own root (name the package's name, path src/main.vr or src/lib.vr, no other key)
V0403a Cargo.toml that cannot be used: it cannot be read, is not valid TOML, has a top-level key cargo reads as a table (workspace, dependencies, features, ...) that is not one, has no [package] name, names with workspace a directory that has no workspace manifest or sits under a Cargo.toml whose workspace is not a table, or its name is not letters, digits, -, and _ starting with a letter or _, or a [package] key has a value of the wrong shape (license = 1), or its rust-version is not MAJOR.MINOR[.PATCH], or is cache, package, or packages, or a program (not a library) is called deps, examples, build, or incremental in any case, the names of Cargo's own build folders, or its version is present but not text of the form MAJOR.MINOR.PATCH that cargo accepts; or its package has both src/main.vr and src/lib.vr (from the command line, a Cargo.toml found but whose package has neither is reported before any check runs: "no Varyk package here" and why, in one line)
V0404a varyk-std line this compiler cannot build with, in a program that uses varyk-std (or in a Varyk package of the build that uses it, at that package's own Cargo.toml, whose lock is not read): missing, from a path or git, optional or renamed, a requirement that is not X.Y or X.Y.Z (optionally ^), or newer than the compiler (the note gives the compiler's version and cargo install varyk). Under varyk publish, also a line on any minor but the compiler's, a Cargo.lock that locks a varyk-std on the compiler's minor older than the compiler, or, when the program or a Varyk package it uses needs varyk-std, an older varyk-std resolved for the build (at the program's varyk-std line, or its Cargo.toml, and not beside the Cargo.lock one); the note gives the line to write, or cargo update -p varyk-std
V0405cargo could not work out which packages the build uses (cargo metadata failed, for example on a path dependency whose folder is missing, or offline with a package not yet downloaded); the note carries cargo's own message
V0406a package whose Cargo.toml does not name its .vr root as its target: no [[bin]] (name the package's name, path = "src/main.vr") or [lib] (path = "src/lib.vr"); the note and the help give the lines to add
V0407the varyk-http package of the build does not match this compiler: its App, Request, or Response is not a struct named at its root, at its Cargo.toml; the note names the package's version, this compiler's, and what is missing, and points to the version table in varyk-http's README
V0900rustc rejected the Rust code Varyk generated, which should not happen, except for the known limits listed under "Calling Rust"; the message carries rustc's own message and code and asks you to report it, or, when rustc also rejected a .rs module you wrote, says it may follow from that error. An error or warning in a .rs module you wrote is not this code: it is shown at your file, unchanged. An error in the crate of a Varyk package the build uses is this code too, naming the package, and is not a Varyk bug when it is in one of that package's own .rs modules
V0901at varyk build: a started call whose task holds a value from Rust code that cannot be sent to, or shared with, another thread (an Rc, Cell, or RefCell inside it), a route or hook call whose handler holds one, or an http::App::new call whose state holds one; the message names the Rust type