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

vyi
import { stdout } from "@io"

fn main() !u8 {
    stdout.write("hello, world\n")
    return Ok(0)
}

Run it:

sh
vyi run hello.vyi

A few things are already visible:

  • Imports bind bare names. import { stdout } from "@io" puts stdout directly in scope — there is no namespace object you dot through. (Why? See methods are scope below.)
  • main returns !u8. That's an error-union: "a u8 or an error." Ok(0) is the success case; an Ok payload of 0 is 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

vyi
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 (i8i64, u8u64); 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:

vyi
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:

vyi
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:

vyi
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:

vyi
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:

vyi
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), or
  • catch — handle it here.
vyi
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:

vyi
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:

vyi
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.

vyi
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:

vyi
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 type

The 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:

vyi
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

sh
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.vyi

Flags come before the entry path.

(Guide 17 — Tooling.)