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
| Type | What it holds |
|---|---|
bool | true or false |
i8, i16, i32, i64 | whole numbers that can be negative, in 8, 16, 32, or 64 bits |
u8, u16, u32, u64 | whole numbers that cannot be negative |
usize | a whole number that cannot be negative: a length, an index, or a count |
f32, f64 | numbers with a fractional part |
string | text |
| a struct you declare | a group of named fields |
| an enum you declare | one 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 |
Error | the 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) |
Time | one point in time in UTC, to the microsecond (see Time, ids, and bytes) |
Uuid | a 128-bit identifier (see Time, ids, and bytes) |
Bytes | an 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>:
| Call | Uses the value | Result |
|---|---|---|
Vec::new() | none | an empty Vec; its type comes from where it goes, as for vec![] |
v.push(x) | changes | nothing; x is kept in v |
v.pop() | changes | Option<T>: the last element, taken out, or None |
v.len() | reads | usize, the number of elements |
v.is_empty() | reads | bool |
v.insert(i, x); i: usize | changes | nothing; x is kept at i, and the elements from i on move up one |
v.remove(i); i: usize | changes | T: the element at i, taken out |
v.contains(x); x read | reads | bool; T is a number type, bool, string, Time, Uuid, or Bytes |
v.sort() | changes | nothing; T is an integer type, bool, string, or Time |
v.join(sep); sep: string read | reads | new text: the elements with sep between them; T is string |
v.get(i); i: usize | reads | Option<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 stored | a chain of the elements (see Chains) |
v[i] | reads, or changes when assigned | the 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:
| Call | Uses the value | Result |
|---|---|---|
s.len() | reads | usize, the length of the text in bytes |
s.clone() | reads | a new copy of the text |
s.is_empty() | reads | bool |
s.contains(p), s.starts_with(p); p: string read | reads | bool |
s.to_uppercase() | reads | new text |
s.replace(from, to); both string, read | reads | new text |
s.trim() | reads; s must be stored, or a literal | the 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 read | reads; s must be stored, or a literal | a 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 read | changes | nothing; t is added to the end of s |
s.parse() | reads | Result<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:
| Call | Uses the value | Result |
|---|---|---|
Error::new(text); text: string | none | a new Error with the message text; text is kept, like a struct field |
Error::with_status(status, text); status: u16, text: string | none | a 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() | reads | the message: part of e, not a copy (see "Returning part of a parameter") |
e.status() | reads | the 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>:
| Call | Uses the value | Result |
|---|---|---|
o.is_some() | reads | bool |
o.unwrap_or(d); d: T | takes | T: the value inside, or d for None |
o.ok_or(e) | takes | Result<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: T | takes | Option<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>:
| Call | Uses the value | Result |
|---|---|---|
r.is_ok(), r.is_err() | reads | bool |
r.ok() | takes | Option<T>: the Ok value, or None for an Err |
r.unwrap_or(d); d: T | takes | T: the Ok value, or d for an Err |
r.map_err(|e| ..); e: E | takes | Result<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>:
| Call | Uses the value | Result |
|---|---|---|
HashMap::new() | none | an empty HashMap; its types come from where it goes |
m.insert(k, v) | changes | Option<V>: the value k had before, or None; k and v are kept |
m.contains_key(k); k read | reads | bool |
m.len() | reads | usize, the number of keys |
m.get(k); k read | reads | Option<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 stored | a 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
- Whole numbers:
0,42. Negative numbers use-:-4. An underscore may separate digits:1_000. - Fractional numbers:
2.5. Digits are required on both sides of the dot. trueandfalse.- Strings in double quotes:
"Hello". The escapes are\n(new line),\t(tab),\\(backslash),\"(double quote), and\0(zero byte). No other escapes and no raw strings.
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:
-xnegates a number;!bis "not" for abool.+ - * / %work on numbers only, and both sides must have the same type.+does not join strings:format!("{}{}", a, b)does.< <= > >=compare numbers of the same type, or twoTimes; aUuidor aByteshas no order (V0200).==and!=compare two values of one type: numbers,bool, strings,Error,Time,Uuid,Bytes, and a struct, an enum, anOption, aResult, aVec, or aHashMapwhose contents can be compared (see Types); anything else is an error naming the field in the way (V0203).&&is "and",||is "or".x as Tconverts a number to another number type: any integer type,usize,f32, orf64, with Rust's rules (a float is truncated toward zero and saturates, an integer wraps to a narrower width:300 as u8is44,-1 as u8is255). Both sides must be number types (V0200): not abool, a string, or a struct. It is how ausizebecomes ani32:let count = v.len() as i32;. A number written on its own beforeasis ani32(or anf64with a decimal point).
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:
| Call | Result |
|---|---|
c.map(|x| ..); x: I | a chain of what the closure gives |
c.filter(|x| ..); x: I, gives a bool | a 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 type | I, the items added up |
c.any(|x| ..), c.all(|x| ..); give a bool | bool: whether the closure gives true for any item, or for every one |
c.find(|x| ..); gives a bool | Option<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:
- borrowed: another name for an element of the stored value, read-only,
like a name a
matchpattern makes: the elements ofv.iter(), the keys or values ofm.keys()andm.values(), unless they are of a Copy type, and the pieces ofs.split(sep), parts ofs; - copies: values of a Copy type (numbers,
bools,Times, andUuids), copied as they are read; - owned: new values a
mapmade.
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:
_, which fits anything;- a name, which fits anything and names it;
- a variant:
Shape::Point,Shape::Circle(r),Shape::Rect(w, h),geo::Shape::Pointfor an enum of modulegeo,Some(x),None,Ok(x), orErr(e), with a pattern for each of the variant's values; - a variant with named fields, naming every field once, each as
name, which names it, orname: pattern, withname: _for one not needed:Event::Click { x: 0, y }. There is no.., so leaving a field out is an error; - an integer, with an optional
-, a string,true, orfalse, which fits that one value; - a range of two integers,
1..=5, which fits both ends and every number between them; the first end must not be above the second.
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.
- Matching something stored never gives it away. A name the pattern makes
for a value of a Copy type is a copy. A name for anything else is another
name for part of the stored value, with the rules of a
letmade from a field: while it is still used, the stored value cannot be changed or given away (V0307), and it cannot be stored or returned except as part of a parameter the function returns (see "Returning part of a parameter") (use.clone()for text). An arm that uses no such name may change the stored value. - Matching a value made right there owns it: the names hold their own values and can be given away, pushed, or returned. The exceptions are a value holding, anywhere inside it, an enum that runs code when it is thrown away (see "Rust enums"), which is only looked at, as if stored; and a string, below.
- Matching a string, stored or made right there, with or without string
literal arms, only looks at it: a name the pattern makes is borrowed
text, which can be read but not stored or returned except as part of a parameter the function returns (see "Returning part of a parameter"), and for a string
made right there, not kept past the
matcheither. Copy it with.clone()to keep it:other => other.clone().
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.
-
Looping over something stored never gives it away, and the stored
Veccannot be changed or given away anywhere inside the loop, whether or not the body uses the loop variable (V0307):for x in v { v.push(1); }is an error, unless the function returns right there: a change followed byreturn, orreturn vitself, is fine. After the loop,vcan change again. To change elements as you go, loop over the positions instead:for i in 0..v.len() { v[i] = 0; }. -
The loop variable is a copy when the elements are of a Copy type; otherwise it is another name for one element, with the rules of a
letmade from an element: it cannot be stored or returned except as part of a parameter the function returns (see "Returning part of a parameter") (use.clone()for text). -
Looping over a value made right there owns it: the variable holds each element in turn and can be given away, pushed, or returned.
-
Looping over a chain binds one item each round, with the rules of its kind: a borrowed item is another name for part of what the chain goes over, a copy is a copy, an owned item is owned. The body cannot change or give away anything the head reads (V0307): what the chain goes over, the argument of
split, and every name a closure of the head reads, numbers andbools included, sofor n in numbers.iter().filter(|n| n < limit) { limit = limit + 1; }is an error. To change one of them,collectthe chain first and loop over theVec. After the loop they can change again, even while a name that took the loop variable's value is still used.
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;.
| Call | Uses the value | Result |
|---|---|---|
log::debug(format, args..); args read | none | nothing: a debug line |
log::info(format, args..); args read | none | nothing: an info line |
log::warn(format, args..); args read | none | nothing: a warning line |
log::error(format, args..); args read | none | nothing: 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:
| Call | Uses the value | Result |
|---|---|---|
assert(cond); cond a bool | none | nothing; the test fails when cond is false |
assert_eq(a, b); a and b read | none | nothing; 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.
- Only an async function can call an async function or use
.await(V0211, whose fix addsasync), and.awaitcannot be written inside a closure, which is not async (V0211). .awaitgoes after a call to an async function,Task::allorTask::all_settled, or a name holding a task (V0212).- Async functions may not call each other in a cycle, directly or through others, and an async function may not call itself (V0214). An async function that adds a route or a hook counts as calling its handler or hook, so a handler that gets back to the function that added it is a cycle too.
- An async function may not return part of a parameter (V0311): everything
it returns is new, so return a copy (
.clone()for text).
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;.
| Call | Uses the value | Result |
|---|---|---|
time::sleep(ms); ms: u64 | none | async, 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:
| Written | Kind | Type | Meaning |
|---|---|---|---|
f(x).await | awaited | T | runs f here and gives its result; nothing is started |
f(x) | started | Task<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:
- as the whole value of a
letwith a name:let a = fetch(1);(notlet _ = .., and not a branch of aniformatchthere); - as what
.detach()is called on:send_email(to).detach();; - as an element of
vec!:vec![fetch(1), fetch(2)]starts two tasks; - as the whole value of the closure of a chain's last
map, followed at once bycollect:ids.iter().map(|id| fetch(id)).collect()starts one task per id.
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
| Call | Uses the value | Result |
|---|---|---|
t.await | takes t | T: waits for the task and gives its result |
t.detach() | takes t | nothing: the task runs on with nobody waiting for it |
Task::all(tasks).await | takes tasks, a Vec of tasks giving T | Vec<T>: waits for every task |
Task::all(tasks).await | takes 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).await | takes tasks, a Vec of tasks giving Result<U, E> | Vec<Result<U, E>>: waits for every task and keeps each outcome |
- A task that is thrown away is stopped. When a name holding a task
goes out of scope before it is awaited or detached, the task's work
stops at its next
.await. A?orreturnthat leaves a function early stops the tasks it started and did not await yet. - A detached task runs until it finishes or the program (or the test) ends, whichever comes first; nobody sees its result. A panic in it is printed and does not stop the program.
- A panic in a task that is awaited continues in the function that awaits it, as if the call had been made there.
- A task stays where it is made. A name holding a task can only be
awaited or detached, once (using it again is V0305), and not inside a
closure, which only borrows it (V0304). A
Vecof tasks, made byvec!orcollect, can only be kept in aletand given, once, toTask::allorTask::all_settled, directly or from thatlet(using it again is V0305). Anything else, indexing,push, aforover it, passing, returning, or giving it to another name, is an error (V0215); so is.awaiton aVecof tasks, and writingTaskas a type: a task's type is always worked out from its call. - A task is always used. A name holding a task, or a
Vecof tasks, that nothing awaits, detaches, or gives toTask::allorTask::all_settledis an error (V0213), so a forgotten.awaitis caught.
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).
Task::allon tasks giving aResultstops at the firstErrto arrive: it gives thatErrat once and stops the tasks still running, soTask::all(tasks).await?passes it on. On other tasks it gives every value.Task::all_settledlets every task run to its end and keeps eachOkandErr. It needs tasks giving aResult(V0200, whose note namesTask::all).
// 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:
| Call | Uses the value | Result |
|---|---|---|
Shared::new(value) | takes value, a struct | Shared<T>: puts the struct where many can read it |
s.clone() | reads s | Shared<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.
- What it holds.
Tis a struct, one of the program's or one from a.rsmodule.Shared::newof anything else, a number, aVec, anOption, or an enum, is an error (V0216), whether or not a type is written; to share such a value, put it in a field of a struct. - Reading through it. The struct's fields and methods are reached
straight through the handle:
config.factor,config.name.len(),config.describe(),match config.kind { .. },for x in config.items { .. }. - Read-only. Everything reached through a
Sharedcan only be read, as through a parameter withoutmut: assigning to it, passing it to amutparameter, calling amut selfmethod or a changing call such aspushon it is an error (V0310). Giving one of its fields to something that keeps it, such aspushor a struct literal, needs a.clone()of the field (V0304), as for a parameter's. - Where it is written.
Shared<T>is written only as the type of a parameter or alet; in a field, a return type, or inside another type (Vec<Shared<T>>) it is an error (V0216). AVecorOptionof handles made withvec!orSomeis fine. - Not a
T. AShared<Config>cannot be passed where aConfigis expected (V0200): pass the fields read through it instead. It cannot be printed or compared (V0203), and cannot go throughjsonorenv(V0210). - Given away.
Shared::newtakes its value, and a started call takes aSharedheld by a name as it takes any value (using the name later is V0305), so give each tasks.clone()and keeps.
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:
| Attribute | Goes before | Meaning |
|---|---|---|
#[rename("key")] | a struct field, or a variant that carries no data | the name used in JSON and the environment instead of the Varyk name |
#[default(value)] | a struct field | the value used when the key or variable is missing |
#[skip] | a struct field | never written or read by json or env |
#[test] | a top-level function | a 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;.
| Call | Uses the value | Result |
|---|---|---|
json::parse(text); text: string read | none | Result<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 read | none | new 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 struct is an object; its keys are the field names, or their
#[rename], written in declaration order; keys it does not have are ignored when read; - a key that is missing when read is
Nonefor anOptionfield, the#[default]value if the field has one, and otherwise an error naming the key; Noneis written asnull, andnullreads asNone;- an enum whose variants carry no data is a string, the variant's name or
its
#[rename]; - a
Vecis an array, and aHashMap<string, V>an object (in no particular key order); - numbers,
bool, andstringare themselves; a number out of range for its type is an error when read (300for au8), never a crash; - a
Timeis a string of RFC 3339 text in UTC, written as{}writes it ("2026-10-07T12:00:00.5Z") and read by the rules ofTime::from_iso; a JSON number is not aTime; - a
Uuidis a string, written in lowercase with hyphens and read in the 36-character form with hyphens, hex digits in either case; - a
Bytesis a string of standard base64 with padding (RFC 4648 section 4), read strictly: another alphabet, missing padding, or a character outside the alphabet is an error.
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;.
| Call | Uses the value | Result |
|---|---|---|
env::parse() | none | Result<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:
- a variable set in the process environment wins;
- otherwise, the value in
.env, if that file has the variable; - otherwise, the field's
#[default], orNonefor anOption, or anErrsuch 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.
| Call | Uses the value | Result |
|---|---|---|
Time::now() | none | the current time, cut to the microsecond |
Time::from_iso(text); text: string read | none | Result<Time, Error>, by the rules below |
Time::from_unix(seconds); seconds: i64 | none | Result<Time, Error>; seconds since 1970-01-01T00:00:00Z; an Err outside the range |
Time::from_unix_micros(n); n: i64 | none | Result<Time, Error>; microseconds since 1970-01-01T00:00:00Z; an Err outside the range |
t.to_iso() | reads | string, the time written as below |
t.to_unix() | reads | i64 seconds, rounded down, so a time before 1970 gives the second it falls in |
t.to_unix_micros() | reads | i64 |
t.add_seconds(n); n: i64 | reads | Result<Time, Error>; n may be negative; an Err outside the range |
t.seconds_since(u); u: Time | reads | i64, t minus u in whole seconds, rounded toward zero; negative when u is later |
text.parse() | reads | Result<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.
| Call | Uses the value | Result |
|---|---|---|
Uuid::new() | none | a new version 7 id: time-ordered, ordered by creation within one process |
Uuid::v7() | none | the same as Uuid::new() |
Uuid::v4() | none | a new random version 4 id |
text.parse() | reads | Result<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.
| Call | Uses the value | Result |
|---|---|---|
Bytes::from_text(text); text: string read | none | the UTF-8 bytes of text, a copy |
Bytes::from_base64(text); text: string read | none | Result<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() | reads | Result<string, Error>, a copy; an Err when the bytes are not UTF-8 |
b.to_base64() | reads | string, standard base64 with padding |
b.len() | reads | usize, the number of bytes |
b.is_empty() | reads | bool |
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
| Call | Meaning |
|---|---|
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).await | answers 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).await | sends 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:
- 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, orUuid; aTimeis written asTime::from_isoreads it, aUuidin its hyphenated form (/orders/{id}withid: Uuid); - a parameter of type
Shared<State>, whereStateis the struct given tohttp::App::new, is the app's state; there is at most one; - a parameter of type
http::Requestis the request itself, to read its headers, cookies, or body (the package may add more types like it); - on
post,put, andpatch, one parameter of a type JSON can hold, a struct, an enum, or aVecorHashMap, is the request's body, read as JSON with the rules and attribute checks ofjson::parse(V0209, V0210); - any other parameter of an integer type,
bool,string,Time,Uuid, or anOptionof 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; anOptionisNonewhen 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:
| Returns | Answer |
|---|---|
| nothing | 204, with no body |
a value json::stringify writes, other than an Option | 200, with the value as JSON |
Option<T> | 200 with T as JSON, or 404 for None |
http::Response | the response as built |
Result<X, Error>, X one of the rows above but nothing | X'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
| Hook | Its 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
- A route is checked before anything runs, by
varyk check, as above. - What a request sends is read by the package, never by the handler: a value that does not fit its type is a 400 before the handler is called.
- No internal message reaches a client: an error without a status is a 500 with a fixed body, and so is a crash inside a handler, which stops only that request; the message is in the log.
- Every request may be answered on another thread, so what a handler holds
while it waits, and the app's state, must be able to go there. Every type
Varyk declares can; a type from Rust code that cannot is V0901 at the
route, or at
http::App::new(see "Calling Rust"). - The package's defaults are the careful ones: limits on a request's size and time, listening on this computer only, and CORS off until it is asked for; a limit on how many requests are answered at once is available, but off until it is asked for, since memory is the platform's to manage. Its README lists them and the calls that change them.
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 type | In Varyk |
|---|---|
bool, i8 to i64, u8 to u64, usize, f32, f64 | the same type, passed by value |
&str | a borrowed string |
&mut String | a mut string |
&'static str parameter | a 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 path | any 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 parameter | an owned string: a literal is copied, an owned string moves |
String return | string |
&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 table | the same Varyk type, given away (moved) |
Result<T, varyk_std::Error> return, written by that full path, where T is in this table | Result<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 Result | Time, 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::Time | Bytes, 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 else | T 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 else | any 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 Bytes | borrowed, or mut |
() return, or none | nothing |
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:
- a
pubfield whose type is in the table above is a field like any other; - a field without
pub(or withpub(crate)) cannot be seen from Varyk; - a
pubfield whose type is not in the table is there but cannot be used: reading or assigning it is an error naming its Rust type; - a struct literal,
matcher::Matcher { .. }, needs every field visible and usable; otherwise make one with a function of the Rust file, such asnew.
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:
key: &'static strtakes only text written in the program (V0217), so invaryk-sqlno input can become part of a query;values: Vec<varyk_std::Value>, last, is written as the arguments after the others,1, namehere, each read and not given away (V0218 for a type that cannot be passed);T: varyk_std::serde::de::DeserializeOwnedis filled from where the result goes,Userfrom the return type here, so the facade reads into a struct it has never seen (V0207 when nothing says the type);value: &TwithT: varyk_std::serde::Serialize + ?Sizedtakes any valuejson::stringifywrites,bohere, read and not given away, so the facade sends out a struct it has never seen (V0210 for a type JSON cannot write);varyk-http'sResponse::jsontakes its value this way;varyk_std::Erroris Varyk's ownError, so?ande.message()work; a function returning a barevaryk_std::Errormakes one, for a facade that makes an error of its own (http::bad_requestis Varyk overError::with_status, not such a function);varyk_std::Time,varyk_std::Uuid, andvaryk_std::Bytesare Varyk'sTime,Uuid, andBytes, so a facade takes and gives them, holds them inpubfields and enum variants, and reads aBytesit is lent as&varyk_std::Bytes.
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:
- a module declared in the current module, or a
usealias; - a standard module or type (
json,Vec, ...); - 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.
--releasebuilds with optimizations. As in Rust, integer arithmetic that overflows stops the program in a normal build but wraps around silently in a--releasebuild.--emit-rustprints every generated file (Cargo.tomland the Rust files), each after a line naming it, and then builds.--message-format=jsonworks with every command and prints errors, and the warnings rustc gives about your.rsmodules, as JSON on standard output, one object per line, instead of the text form; undervaryk runandvaryk test, whose standard output is the program's or the test runner's own, they go to standard error instead. The error, each of its labels, and its fix-it each name the file they point into. Lines count from 1;columnfields count bytes from the start of the line, also from 1. A Rust compiler error in one of your.rsmodules is an object withlevel,file(your file),line,column,message,rustc_code(nullwhen rustc gives none), andnotes, rustc'shelp:andnote:lines, each a string starting with its level (help: use ...); any other compiler message is an object withlevelandmessage. Errors of the command itself (no package here, a library to run, a file that cannot be read,cargothat cannot be started) stay one line of text on standard error, and so does cargo failing before it compiles anything, followed by cargo's own words.
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.
| Code | Meaning |
|---|---|
| V0001 | a 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 |
| V0002 | unexpected token or malformed syntax |
| V0003 | a 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) |
| V0012 | lifetime syntax such as <'a> or &'a T; lifetimes are worked out by the compiler |
| V0100 | unknown 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 |
| V0101 | unknown 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 |
| V0102 | unknown field, of a struct or of a variant with named fields, in a value or a pattern |
| V0103 | a 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) |
| V0104 | a 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 |
| V0105 | an 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 |
| V0106 | a 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 |
| V0107 | String or str written where string is meant |
| V0108 | a 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 |
| V0109 | a struct or enum that contains itself, directly or through other structs, enums, Option, or Result; a Vec or HashMap breaks the cycle |
| V0110 | a 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 |
| V0111 | a 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) |
| V0112 | an 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)]) |
| V0113 | the 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 |
| V0114 | a #[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 |
| V0115 | a 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 |
| V0200 | type 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 |
| V0201 | wrong 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 |
| V0202 | println!, 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 |
| V0204 | a 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 |
| V0205 | a 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 |
| V0207 | a 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 |
| V0208 | a 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) |
| V0209 | a #[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 |
| V0210 | a 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 |
| V0211 | a 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 |
| V0213 | a 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 |
| V0214 | async 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 |
| V0215 | a 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 |
| V0216 | Shared 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) |
| V0217 | an 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 |
| V0218 | a 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) |
| V0219 | a 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) |
| V0220 | a 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 |
| V0221 | a 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 |
| V0222 | a 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 |
| V0300 | changing a parameter that was declared without mut, by assigning to it or calling push or pop on it |
| V0301 | changing 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 |
| V0302 | a 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 |
| V0303 | a 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 |
| V0304 | a 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() |
| V0305 | a 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 |
| V0306 | a 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 |
| V0307 | a 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) |
| V0308 | a 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 |
| V0309 | a started call passing a value to a mut parameter, or calling a mut self method: the task would change only its own copy |
| V0310 | changing 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 |
| V0311 | an async function that returns part of a parameter; return a copy instead |
| V0400 | a package whose Cargo.toml does not say edition = "2024" |
| V0401 | something 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 |
| V0402 | a 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) |
| V0403 | a 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) |
| V0404 | a 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 |
| V0405 | cargo 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 |
| V0406 | a 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 |
| V0407 | the 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 |
| V0900 | rustc 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 |
| V0901 | at 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 |