Guides · 01
Introduction
Vyi is a small systems language in early alpha. It has value semantics, typed
absence (?T), a compile-time interpreter that runs your code during compilation,
and a method system built on lexical scope. It compiles to WebAssembly and to
native linux (x86-64 and arm64).
This guide gets you from nothing to writing real programs, and introduces the ideas that define how Vyi works. The later guides go deep on each feature.
Hello, world
import { stdout } from "@io"
fn main() !u8 {
stdout.write("hello, world\n")
return Ok(0)
}Run it:
vyi run hello.vyiA few things are already visible:
- Imports bind bare names.
import { stdout } from "@io"putsstdoutdirectly in scope — there is no namespace object you dot through. (Why? See methods are scope below.) mainreturns!u8. That's an error-union: "au8or an error."Ok(0)is the success case; anOkpayload of0is the process exit code. We'll unpack errors shortly.- Strings are UTF-8. A
""literal is a baked array of its bytes that flows into a[*]u8(a pointer-plus-length view). Chars use single quotes:'a'.
Bindings: con, mut, type
con n = 42 // immutable binding, type inferred (i32)
mut total = 0 // mutable
total = total + n
con pi: f64 = 3.14159 // explicit type
type Id = u32 // a type binding (also used for generics — see below)con is the default; reach for mut only when you rebind. There is no int — integer
widths are explicit (i8–i64, u8–u64); i32 is the conventional default in user
code.
Functions and methods
Functions are declared with fn. A method is just a function with a receiver
written before the name:
import { stdout } from "@io"
struct Point { x: i32, y: i32 }
// instance method — receiver is a value (or *mut Point / *con Point for a pointer receiver)
fn (p: Point) sum() i32 {
return p.x + p.y
}
fn main() !u8 {
con p = Point{ x: 3, y: 4 }
stdout.writeInt(i64(p.sum())) // prints 7
stdout.write("\n")
return Ok(0)
}The receiver position is the key idea: methods are free-standing top-level declarations, not members nested inside the type. That means anyone — including a third-party module — can declare a method on any type, and which methods a type has is determined by what's in scope, not by the type's definition. (Guide 04 and 14 go deep; this is also why imports bind bare names instead of namespace objects.)
Functions can be overloaded by full signature, take default arguments, and infer
omitted types with _. Functions are also first-class values — closures capture their
environment by value (guide 04).
Structs, enums, switch
Structs are nominal. Enums carry uniquely-named variants, which may hold payloads, and
you consume them with switch:
enum Shape {
Dot,
Circle(i32), // payload: radius
Rect(i32, i32), // payload: w, h
}
fn (s: Shape) area() i32 {
switch s {
Dot: { return 0 },
Circle(r): { return 3 * r * r },
Rect(w, h): { return w * h },
}
}switch is exhaustive, and arms bind payloads directly. It's also an expression — see
control flow below.
Structs also convert by shape: a wider struct implicitly copies into a shape-compatible narrower one, dropping the extra fields:
import { stdout } from "@io"
struct Point { x: i32, y: i32 }
struct Rect3D { x: i32, y: i32, z: i32 }
fn sum(p: Point) i32 { return p.x + p.y }
fn main() !u8 {
con r = Rect3D{ x: 1, y: 2, z: 3 }
con a = sum(r) // implicit copy Rect3D → Point; z is dropped
con p: Point = r // same on an initialiser
stdout.writeInt(i64(a + p.y)) // prints 5
stdout.write("\n")
return Ok(0)
}It's a copy, not aliasing — the source is unchanged. This is the one large implicit conversion in the language (guide 03).
Absence is ?T
A maybe-absent value is an Optional, spelled ?T:
fn first(xs: [*]i32) ?i32 {
if xs.len == 0 {
return None
}
return Some(xs[0])
}None and Some aren't keywords — they are ordinary variants of the Optional enum
behind ?T. When the expected type at a use site is an enum, you can name its variants
bare; that works for every enum, yours included.
A *T is therefore always valid by construction; a pointer that might be absent is
?*T. (Guide 07, 09.)
Errors are inferred, and carried by value
An error-union is E!T — "an E or a T." Write the error set explicitly, or omit it
and let the compiler infer it:
enum ParseError { Empty, BadDigit }
fn parse(s: [*]u8) ParseError!i32 {
if s.len == 0 {
return Err(Empty)
}
mut n = 0
for c in s {
if c < u8('0') || c > u8('9') {
return Err(BadDigit)
}
n = n * 10 + (i32(c) - 48)
}
return Ok(n)
}Bare !T (no explicit error set) desugars to Result(_, T), where _ is an inferred
error set — the minimal union of the error types the body actually produces (Err(e))
or propagates. You consume a fallible value with:
try— propagate the error to the caller (short-circuit), orcatch— handle it here.
enum ParseError { Empty }
fn parse(s: [*]u8) ParseError!i32 {
if s.len == 0 { return Err(Empty) }
return Ok(i32(s[0]))
}
fn parseTwo(a: [*]u8, b: [*]u8) !i32 {
con x = try parse(a) // on error, return it from parseTwo
con y = parse(b) catch { 0 } // on error, use 0
return Ok(x + y)
}There's no hidden boxing: the error rides in the result's payload by value. (Guide 07; precise rules in the error model reference.)
Control flow is expression-oriented
if and switch are expressions — they produce values:
import { stdout } from "@io"
enum Shape { Dot, Circle(i32), Rect(i32, i32) }
fn main() !u8 {
con n = 5
con label = if n > 0 { "positive" } else { "non-positive" }
con s = Shape.Rect(2, 3)
con sides = switch s {
Dot: { 0 },
Circle(_): { 0 },
Rect(_, _): { 4 },
}
stdout.writeInt(i64(sides)) // prints 4
stdout.write(" ")
stdout.write(label) // prints positive
stdout.write("\n")
return Ok(0)
}Loops come in a few forms — a bare condition (for cond { }), and for x in xs { } over
ranges, arrays, and anything that implements the iteration protocol:
mut sum = 0
for i in 0..10 { sum = sum + i } // 0..10 is exclusive; 0...10 is inclusive(Guide 05.)
() is unit
A function that returns nothing returns (), the empty tuple — Vyi has no void
keyword. () is a real zero-size value you can name, store, and pass.
import { stdout } from "@io"
fn log(msg: [*]u8) { // omitted return type means ()
stdout.write(msg)
}Known values: computing at compile time
The compiler contains a real interpreter, and Vyi has a name for the values it produces:
a known value is any value the compiler computes at compile time. This is core
vocabulary — you'll meet it everywhere (known parameters, the known operator,
known-value stability), so learn it now: known means resolved during
compilation, as opposed to a runtime value that only exists when the program runs.
type is itself a known value — the type whose values are types — and generics fall
straight out of that. A generic type is just a function that takes a known type and
returns a type:
fn Box(known T: type) type { // T is a known value (a type), fixed at compile time
return struct { value: T }
}
con b = Box(i32){ value: 7 } // Box(i32) is evaluated at compile time → a concrete typeThe interpreter that produces known values is parkable: it evaluates on demand and suspends on anything not yet resolved, so nothing needs to be declared before it's used — across files, in any order. (Guide 11 — Known values and generics.)
Interfaces capture methods at the cast site
An interface is a behavioural shape. Any type with matching methods satisfies it — no
implements keyword:
import { stdout } from "@io"
interface Speaker {
fn speak() i32,
}
struct Dog { volume: i32 }
fn (d: Dog) speak() i32 { return d.volume }
fn main() !u8 {
con d = Dog{ volume: 42 }
con s: Speaker = &d // box into the interface
stdout.writeInt(i64(s.speak())) // dynamic dispatch → prints 42
stdout.write("\n")
return Ok(0)
}Because methods live in scope, boxing a value into an interface captures the methods resolved at that cast site into the interface's vtable. The interface value is a witness table built from the scope where you boxed it. (Guide 12.)
Where to go next
- Need the toolchain first? Installing Vyi covers the install script and building from source.
- New to the language? Continue with 02 — Values and bindings and read the guides in order.
- Coming from Zig, Rust, Go, or Swift? Read Coming from another language first — it maps your existing mental model onto Vyi (the field-dropping copy, scope-based methods, parkable compile-time evaluation, inferred errors, typed absence).
- Want the precise rules? Jump to the reference.
The CLI in one screen
vyi run file.vyi # build and run (wasm by default)
vyi run --target x86-64-linux file.vyi # native
vyi build -o out file.vyi # build an artifact
vyi build --emit=wat file.vyi # emit WebAssembly text
vyi check file.vyi # type-check only (--json for tooling)
vyi test # run *.tests.vyiFlags come before the entry path.
(Guide 17 — Tooling.)