Guides · 01
Introduction
Vyi is a small systems language in early alpha. Types and values share one
syntax, a compile-time interpreter runs your code during compilation, and
methods resolve from lexical scope instead of belonging to types. Absence is
typed (?T), structs have value semantics, and 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 { println } from "@fmt"
fn main() !u8 {
println("hello, world")
return Ok(0)
}Run it:
vyi run hello.vyiA few things are already visible:
- Imports bind bare names.
import { println } from "@fmt"putsprintlndirectly 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[*con]u8(a pointer-plus-length view). Chars use single quotes:'a'. printlntakes any value that renders itself, not a format string —println(42)works, andprintln($"n = ${n}")interpolates (guide 10).
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 { print, println } from "@fmt"
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 }
print(p.sum()) // prints 7
println("")
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 { print, println } from "@fmt"
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
print(a + p.y) // prints 5
println("")
return Ok(0)
}It's a copy, not aliasing — the source is unchanged. Several conversions happen implicitly (a narrower union widens, a literal takes its context's width), but this is the only one that discards data (guide 03).
Absence is ?T
A maybe-absent value is an Optional, spelled ?T:
fn first(xs: [*con]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: [*con]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: [*con]u8) ParseError!i32 {
if s.len == 0 { return Err(Empty) }
return Ok(i32(s[0]))
}
fn parseTwo(a: [*con]u8, b: [*con]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"
import { print, println } from "@fmt"
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 },
}
print(sides) // prints 4
print(" ")
stdout.write(label) // prints positive
println("")
return Ok(0)
}for is the only loop keyword, in three forms: infinite (for { }), 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 { print } from "@fmt"
fn log(msg: [*con]u8) { // omitted return type means ()
print(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 { print, println } from "@fmt"
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
print(s.speak()) // dynamic dispatch → prints 42
println("")
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's deltas (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 linux-x64 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.)