Docs · Appendix

Coming from another language

You already have a mental model — this page maps it onto Vyi's. Each section below takes one language and covers the ideas that transfer, the ideas that almost transfer — the ones worth reading closely — and a table of spellings.

The distinctly Vyi deltas

Whatever you're coming from, these are the Vyi facts with no common ancestor:

  • A wider struct implicitly copies into a narrower shape-compatible one,

dropping extra fields, at every value-flow site. A copy, never aliasing. (03 — Structs, enums, tuples)

  • Methods are scope, not type members. Free-standing declarations with a

receiver slot; which methods a type has depends on what's in scope, and anyone can extend any type. (04, 14)

  • Declarations need no ordering. The compiler evaluates known values on

demand and suspends on anything unresolved, so declarations reference each other in any order, across files. (11 — Known values and generics)

  • Bare !T infers its error set — the minimal union of what the body

actually produces or propagates. (07 — Optionals and errors)

  • Absence is ?T. A pointer is always valid by construction.
  • () is the unit type; switch is the branching form; the

catch-all arm is any.

Coming from Zig

Vyi and Zig want the same things — explicit memory, errors as values, no hidden control flow, compile-time evaluation as a first-class tool — so most of your instincts carry over. The vocabulary shifts in a few places.

Where Zig evaluates code at compile time under the comptime keyword, Vyi's concept is the known value: any value the compiler computes at compile time. The overlap is real — fn max(known T: type, a: T, b: T) T reads exactly like the Zig you'd write — but known is a property of values and calls, not a context you enter. Any ordinary function can be folded at compile time in one place (known dbl(3)) and called at runtime in another; generics are ordinary functions over type values; and known values have one ability with no Zig counterpart: pointing at one reifies it into the program's data, so the pointer is stable. (11 — Known values and generics)

Errors are close but simpler: there is no special error-set kind and no error{…} declaration. A Vyi error is a plain enum (any type, in fact — and it can carry payload data), E!T is the error union, and bare !T is the counterpart of your inferred error set: the compiler infers the minimal union of error types the body produces. try and catch do what you expect; Zig's orelse is also catch, because optionals and error unions are consumed by the same operators.

Allocators exist and are explicit — Allocator is an interface from @mem, with systemAllocator, ArenaAllocator, and FixedBufferAllocator behind it — but there is no allocator-in-every-signature convention. Containers take an allocator at construction and carry it (Vec(i32).new(&arena)), so signatures downstream don't thread one through. (09 — Pointers and memory)

You write (Zig)In Vyi it's
const x = 42;con x = 42
var x: i32 = 0;mut x = 0
fn max(comptime T: type, a: T, b: T) Tfn max(known T: type, a: T, b: T) T
comptime { … }known { … } (a value-producing block)
error{NotFound}!i32enum E { NotFound }E!i32
inferred error set !i32bare !i32 — inferred minimal union
try f()try f()
f() catch 0f() catch 0
opt orelse 0opt catch 0
opt.?no force-unwrap — switch, if Some(v) = opt, try, or catch
fn f(allocator: Allocator, …) everywhereallocators are explicit but ride inside containers
switch with elseswitch with any
defer (runs at scope exit)defer (runs at function exit)
x: i32 = undefined;doesn't exist — every binding takes a value

One worked delta — no error-set declaration anywhere, and the union is still precise:

vyi
import { stdout } from "@io"

enum ParseError { BadDigit }

fn parse(s: [*]u8) !i32 {          // error side inferred: exactly ParseError
    if s[0] < u8('0') { return Err(ParseError.BadDigit) }
    return Ok(i32(s[0]) - 48)
}

fn main() !u8 {
    con n = parse("7") catch 0
    stdout.writeInt(i64(n))
    stdout.write("\n")
    return Ok(0)
}

Coming from Rust

The biggest unlearning: there are no moves and no borrows. Vyi is a by-value language — assignment copies, arguments copy, returns copy — and the original stays usable after every one of them. There are no lifetimes to annotate; in their place is one compile-time rule, stability: a pointer into the current stack frame may not escape it (returning &local is rejected), while pointers to allocator memory, globals, parameters, and known values may go anywhere. Liveness past that — use-after-free, an arena pointer after reset() — is yours to manage, allocator by allocator. (09 — Pointers and memory)

impl blocks don't exist. A method is a free-standing fn with a receiver slot, resolved through lexical scope — which means the orphan rule doesn't exist either: you can declare methods on any type from any file, including types you don't own, and it's visible exactly where it's imported. Traits become interfaces: structural (no impl Trait for T — matching methods in scope is satisfaction), and the vtable is captured at the cast site from whatever is in scope there, so two modules can give the same type different behaviour without conflict. Option and Result translate almost verbatim — they're plain enums in @core, with sugar ?T and E!T and the postfix ? replaced by a prefix try. (12 — Interfaces, 07 — Optionals and errors)

There is no Drop. Cleanup is defer, written where the resource is acquired and run on every exit path. And where Rust infers a generic from <T>, Vyi's generic parameter is an ordinary compile-time argument — usually inferred from the call, exactly like your turbofish-less calls.

You write (Rust)In Vyi it's
let x = 5;con x = 5
let mut x = 5;mut x = 5
Option<i32> / Some(5) / None?i32 / Some(5) / None (ordinary enum variants)
Result<i32, E>E!i32 — or bare !i32, error set inferred
f()?try f()
f().unwrap_or(0)f() catch 0
matchswitch (exhaustive; catch-all is any)
impl Point { fn norm(&self) -> i32 }fn (p: Point) norm() i32 — free-standing
trait Speak + impl Speak for Doginterface Speaker { fn speak() i32, } — satisfied structurally
&dyn Speakcon s: Speaker = &d — vtable captured at the cast site
fn max<T>(a: T, b: T) -> Tfn max(known T: type, a: T, b: T) T
Vec<i32> / HashMap<K, V>Vec(i32) / HashMap(K, V) — allocator-backed, from @core
Dropdefer at the acquisition site
lifetimes <'a>none — stability rule + allocator contracts

Copies, not moves — both names stay live:

vyi
import { stdout } from "@io"

struct Point { x: i32, y: i32 }

fn (p: Point) norm1() i32 {        // methods are free-standing — no impl block
    return p.x + p.y
}

fn main() !u8 {
    con p = Point{ x: 40, y: 2 }
    con q = p                      // a copy, not a move — p is still usable
    stdout.writeInt(i64(p.norm1() + q.x - q.x))
    stdout.write("\n")
    return Ok(0)
}

Coming from Go

The shape of Vyi code will feel familiar — fn with the return type at the end, receivers in parentheses, structural interfaces, defer, panic/recover — but two foundations differ.

First, there is no garbage collector. Memory beyond the current stack frame comes from an explicit allocator (@mem), and containers take one at construction. Closures still escape freely — capture is a by-value snapshot carried inline in the function value, no heap involved. (09 — Pointers and memory)

Second, errors live in the type, not in a convention. Instead of returning (T, error) by convention, a fallible function returns E!T — and the compiler makes "forgot to check" impossible: the success value is only reachable through try, catch, or a switch. absence is ?T, checked the same way. (07 — Optionals and errors)

The rest is refinement. defer is function-scoped in both languages, with one timing delta: Go evaluates a deferred call's arguments at the defer statement, while a Vyi defer body evaluates entirely at exit, seeing the locals' final values. recover keeps its Go meaning but is stricter — it's a form usable only inside a defer body, and it binds the panic's reason string directly. Methods shed Go's same-package restriction: any file can declare methods on any type, including primitives, and interface satisfaction is checked — and captured — at the place you convert. Goroutines and channels have no counterpart today — concurrency is (planned) (18 — Concurrency).

You write (Go)In Vyi it's
x := 5 / var x intcon x = 5 / mut x = 0 (no zero-value defaults)
func (p Point) Sum() intfn (p: Point) sum() i32
v, err := f(); if err != nil { return err }con v = try f()
v, err := f(); if err != nil { v = 0 }con v = f() catch 0
defer f.Close()defer f.close() — function-scoped in both
panic("boom") / recover()panic "boom" / recover(reason) { … } inside a defer
nilnone — absence is ?T / ?*mut T
implicit interface satisfactionsame idea; methods captured at the cast site: con s: Speaker = &d
methods only in the type's packagemethods on any type, from any file
[]int[*]i32 — pointer + length, bounds-checked
map[string]intHashMap(String, i32) — explicit allocator
switch (non-exhaustive, default)switch (exhaustive, any)
go f() / channelsno counterpart today (planned)

The error delta in one function:

vyi
import { stdout } from "@io"

enum LoadErr { Missing }

fn load(k: i32) LoadErr!i32 {      // the error is in the type, not a convention
    if k == 0 { return Err(Missing) }
    return Ok(k * 2)
}

fn main() !u8 {
    con v = load(21) catch 0       // no `if err != nil` — handle or propagate
    stdout.writeInt(i64(v))
    stdout.write("\n")
    return Ok(0)
}

Coming from Swift

Vyi will feel like the struct-and-enum half of Swift with the class half removed. Value semantics are the kinship: structs and enums copy on assignment, enums carry associated values (Vyi says payloads), and switch is exhaustive with patterns that bind them. Optionals translate almost one-to-one — ?T for T?, if Some(v) = x for if let v = x, x catch 0 for x ?? 0 — with one difference: Some/None are ordinary enum variants you write explicitly; there is no implicit wrapping of a T into a T?. (07 — Optionals and errors)

Sharing is an explicit pointer (*mut T / *con T), and memory beyond the stack comes from an explicit allocator (09 — Pointers and memory). Protocols map to interfaces, with two twists: satisfaction is structural (matching methods in scope is enough), and the method table is captured at the cast site from lexical scope, so extending someone else's type is declaring a method near your code — Vyi's form of extension. (12 — Interfaces)

Errors move from the effects system into the return type: where Swift marks throws and handles with do/catch, Vyi returns E!T and handles with a catch expression — no separate do block, and propagation is the same try you already type.

You write (Swift)In Vyi it's
let x = 5con x = 5
var x = 5mut x = 5
T? / if let v = x?T / if Some(v) = x
x ?? 0x catch 0
x!no force-unwrap — handle it, or catch { panic "…" }
guard let v = x else { return }if-let with an early-return else
enum Shape { case circle(Int) }enum Shape { Circle(i32) }
exhaustive switch with defaultexhaustive switch with any
protocol + declared conformanceinterface — structural, captured at the cast site
extension Point { func norm() … }fn (p: Point) norm() i32 — anywhere, no wrapper
func f() throws -> Int + do/catchfn f() E!i32 + try / catch expression
struct methods inside the typefree-standing fn (p: Point) name()
classes + ARCnone — values, pointers, explicit allocators

The optional kinship, verbatim:

vyi
import { stdout } from "@io"

fn main() !u8 {
    con x: ?i32 = Some(40)
    if Some(v) = x {               // Swift's `if let v = x`
        stdout.writeInt(i64(v + 2))
        stdout.write("\n")
        return Ok(0)
    }
    return Ok(0)
}

Where to next

The guides are written to be read in order from 01 — Introduction; with a systems-language background you can move fast until guides 11 (known values) and 12 (scope-captured interfaces) — those two are where Vyi is most itself. The glossary pins down every term.