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
!Tinfers 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;switchis 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) T | fn max(known T: type, a: T, b: T) T |
comptime { … } | known { … } (a value-producing block) |
error{NotFound}!i32 | enum E { NotFound } … E!i32 |
inferred error set !i32 | bare !i32 — inferred minimal union |
try f() | try f() |
f() catch 0 | f() catch 0 |
opt orelse 0 | opt catch 0 |
opt.? | no force-unwrap — switch, if Some(v) = opt, try, or catch |
fn f(allocator: Allocator, …) everywhere | allocators are explicit but ride inside containers |
switch with else | switch 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:
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 |
match | switch (exhaustive; catch-all is any) |
impl Point { fn norm(&self) -> i32 } | fn (p: Point) norm() i32 — free-standing |
trait Speak + impl Speak for Dog | interface Speaker { fn speak() i32, } — satisfied structurally |
&dyn Speak | con s: Speaker = &d — vtable captured at the cast site |
fn max<T>(a: T, b: T) -> T | fn max(known T: type, a: T, b: T) T |
Vec<i32> / HashMap<K, V> | Vec(i32) / HashMap(K, V) — allocator-backed, from @core |
Drop | defer at the acquisition site |
lifetimes <'a> | none — stability rule + allocator contracts |
Copies, not moves — both names stay live:
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 int | con x = 5 / mut x = 0 (no zero-value defaults) |
func (p Point) Sum() int | fn (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 |
nil | none — absence is ?T / ?*mut T |
| implicit interface satisfaction | same idea; methods captured at the cast site: con s: Speaker = &d |
| methods only in the type's package | methods on any type, from any file |
[]int | [*]i32 — pointer + length, bounds-checked |
map[string]int | HashMap(String, i32) — explicit allocator |
switch (non-exhaustive, default) | switch (exhaustive, any) |
go f() / channels | no counterpart today (planned) |
The error delta in one function:
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 = 5 | con x = 5 |
var x = 5 | mut x = 5 |
T? / if let v = x | ?T / if Some(v) = x |
x ?? 0 | x 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 default | exhaustive switch with any |
protocol + declared conformance | interface — structural, captured at the cast site |
extension Point { func norm() … } | fn (p: Point) norm() i32 — anywhere, no wrapper |
func f() throws -> Int + do/catch | fn f() E!i32 + try / catch expression |
struct methods inside the type | free-standing fn (p: Point) name() |
| classes + ARC | none — values, pointers, explicit allocators |
The optional kinship, verbatim:
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.