VYB_
Open Source · Systems Programming · LLVM

Vyb Programming Language
Systems Programming

Vyb is an open-source systems programming language developed by Aniviza.

Vyb is the open-source systems programming language developed by Aniviza LLC. The canonical project site is vyb-lang.org; source and maintained documentation live in the rickenator/Vyb GitHub repository.

Ask DeepWiki

Vyb Programming Guide#

Where the docs live. This README is the project overview. The canonical, maintained reference for every language feature is the docs/refman/PROGRAMMERS_GUIDE.md — language tour, ownership & polymorphism, closure capture forms, async (Future<T> / await / async lambdas), the memory model, and a full standard-library tour, with a core-feature registry (§1.1) that maps each feature to its section and is updated when core features change. Exact per-symbol signatures live in the auto-generated pages under docs/refman/.


1. Introduction#

Welcome to the Vyb programming language version 0.7.7. This overview walks you through what Vyb is and the thinking behind it. Vyb is a systems language built on a simple conviction: you should not have to choose between safety and power. Strong type safety and explicit ownership give your code discipline and predictability, while low-level escape hatches keep raw, machine-near control within reach when you need it. Code runs as fast native executables or through a JIT, so performance stays predictable from first prototype to final program. The syntax is readable and name-first, the abstractions are zero-cost, and the language aims to stay out of your way as your ambitions grow. The rest of this guide shows how those ideas take shape through feature summaries and examples.

1.1 Purpose & Audience#

This guide is intended for systems programmers, language designers, and developers who want:

Vyb is a systems language with a template-driven, name-first surface: monomorphized generics, ownership-qualified types (my/our/their/mild), freedom blocks for raw control, and a single toolchain that JITs, compiles AOT and links native executables.

Here is how Vyb's language surface compares against several modern systems languages:

Language Templates / Generics Memory Model Concurrency Syntax Style Unique Feature Comment
Vyb Monomorphized generics everywhere Ownership types (my/our/mild/their), reference counting Async/await, threads, channels, agents Name-first syntax name(params)<Type> ->, aspect/bind polymorphism select expressions, defer, freedom blocks, fail/trap error system Combines zero-cost generics with readable ownership semantics
Rust Monomorphized generics Ownership/borrow checker; optional Arc/Rc async/await, threads, channels C-style braces, macros Zero-cost abstractions; strong compile-time safety No global GC; all memory safety enforced at compile time
D Runtime & compile-time templates GC by default; @nogc for manual alloc/free std.concurrency fibers, threads C-style; mixins Compile-time function execution (CTFE) Blend of high-level features with systems control
C++ Templates & concepts (20+) Manual new/delete; smart pointers (unique_ptr, shared_ptr) Threads, coroutines (co_await) C-style braces Metaprogramming via templates & concepts Extensive ecosystem; highest portability
Nim Generics + macros GC by default; optional manual alloc Async (async/await), threads Python-like indentation Hygienic macros; optional GC or ARC Very concise syntax; strong metaprogramming support
Go Generics (1.18+) GC only Goroutines, channels C-style, minimal CSP-style concurrency Simple, fast compile; built-in tooling

Note: Each language offers a different balance of safety, performance, and ergonomics. Vyb's strength lies in unifying monomorphized generics, ownership types with low-level freedom, and a single toolchain that JITs, compiles AOT, and links native executables — in a terse, self-hostable package.

1.2 What is Vyb?#

Vyb is a statically typed, compiled systems language targeting native code via LLVM. Its key differentiators:

Parameter passing is explicit. A plain x<T> parameter is a value copy — the callee works on its own copy and the caller's variable is untouched (a Vec or an owning struct is deep-copied; a String shares its buffer; an our<T> bumps its refcount). To read or mutate in place without copying, take a their<T> borrow, supplied as borrow(x) (mutable) or view(x) (read-only their<T const>). Ownership is separate from reference-passing: my<T> is unique ownership (a named my passed to a my parameter moves — use-after-move is rejected), and our<T> is shared, ref-counted ownership; neither is another spelling of ref. The full, tested rules are in the “Parameter passing” section of the Programmer’s Guide.

Quick Start#

# Option A: official binary SDK (Linux x86_64) — download the newest
# vyb-sdk-<version>-linux-x86_64.tar.gz from the release page, unpack it, and
# put its bin/ on your PATH:
#   https://github.com/rickenator/Vyb/releases   (all releases)
#   releases are cut from the version tags: https://github.com/rickenator/Vyb/releases/tag/v0.7.7
# There is no macOS SDK yet: the runtime's async/fibre layer is Linux-only
# (ucontext/pipe2), so the release workflow cannot build it. See docs/sdk/INSTALL.md.

# Option B: build from source (requires LLVM 18 + CMake)
git clone https://github.com/rickenator/Vyb.git
cd Vyb
mkdir -p build && cd build && LLVM_DIR=/usr/lib/llvm-18/cmake cmake .. && make -j$(nproc) && cd ..

# Run the full test suite (1314 .vyb tests) with the canonical Vyb runner
build/vyb test/run_tests.vyb --vyb build/vyb --test-dir test

# Run your first Vyb program
echo 'main()<Int> -> { return 42 }' > hello.vyb
build/vyb hello.vyb  # Prints 42 and exits 0

# Emit CUDA kernels: pure NVPTX device code, validated by ptxas as it is emitted
build/vyb --kernel fixtures/kernel/axpy.vyb --ptx axpy.ptx

# Try select expressions with pattern matching
cat > example.vyb << 'EOF'
main()<Int> -> {
    numbers<Vec<Int>> = Vec()
    numbers.push(10)
    numbers.push(20)

    result<Int> = select(numbers.len()) -> {
        0 -> 0,
        2 -> {
            sum<Int> = numbers.get(0) + numbers.get(1)
            pass sum
        },
        ? -> -1
    }
    return result
}
EOF
build/vyb example.vyb  # Prints 30 and exits 0

# Complex return types with auto-serialization
echo 'main()<Int,String> -> { return 42, "Hello!" }' > tuple.vyb
build/vyb tuple.vyb  # Outputs: [42, "Hello!"]

Pointing a project at this checkout#

Vyb owns the toolchain paths, so a project that builds against it does not need to hardcode the checkout. The toplevel SOURCEME_VYB resolves this checkout and exports the two values every consumer needs (VYBHOME plus the derived VYB and VYB_STDLIB):

. "$HOME/Projects/Vyb/SOURCEME_VYB"   # one line, e.g. in your shell profile
# VYBHOME=/home/…/Vyb   VYB=$VYBHOME/build/vyb   VYB_STDLIB=$VYBHOME/stdlib

An explicit VYBHOME wins. Otherwise the home is derived from an existing VYB/VYB_BIN (a compiler binary, its build/ directory, or the checkout root), and otherwise from SOURCEME_VYB's own location — so the checkout can live anywhere. Sourcing is idempotent and touches only those three variables (PATH is left alone; the packaged SDK's sdk/env.sh is the one that prepends bin/). A strictly POSIX shell cannot report a sourced file's path, so set the home first: VYBHOME=/path/to/Vyb . /path/to/Vyb/SOURCEME_VYB. Covered by test/vybenv_smoke.sh in CI.

Compilation to Native Code#

Vyb provides a complete compilation pipeline from source to standalone executables:

Building Standalone Executables#

# Build executable (simplest form)
build/vyb hello.vyb --build hello
./hello  # Run directly!

# Build with custom name
build/vyb program.vyb --build myapp
./myapp

# Build with optimization
build/vyb program.vyb -b myapp -O3  # Maximum optimization

# Static linking (fully standalone, no runtime dependencies)
build/vyb program.vyb -b myapp --static

# What happens:
# 1. Compiles hello.vyb → hello.o (LLVM object file)
# 2. Compiles runtime/vyb_runtime.c → vyb_runtime.o (C runtime)
# 3. Links with system linker (lld/ld) + CRT files + libc/libm
# 4. Creates executable: hello

# Verify the executable
file myapp
# Output: myapp: ELF 64-bit LSB executable, x86-64, dynamically linked, with debug_info

ldd myapp
# Output:
#   linux-vdso.so.1 (0x00007fff...)
#   libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
#   libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f...)

Projects (vyb.toml)#

For multi-file projects, vyb.toml describes the package and every [[bin]], and vyb build compiles and links the whole project in one step:

[package]
name = "myapp"
version = "0.1.0"

[[bin]]
name = "myapp"
path = "src/main.vyb"

[dependencies]
mylib = { path = "../mylib" }
# Scaffold a new project
vyb new myapp          # creates myapp/vyb.toml + myapp/src/main.vyb

# Build every [[bin]] from the manifest (src/ and local path deps are added to
# the module search paths automatically; resolved deps are pinned in vyb.lock)
cd myapp && vyb build
./target/myapp

Compiling to Object Files#

# Compile to object file (default -O2 optimization)
build/vyb hello.vyb --compile hello.o

# Specify optimization level
build/vyb program.vyb -c program.o -O0  # No optimization (fast compile)
build/vyb program.vyb -c program.o -O1  # Basic optimization
build/vyb program.vyb -c program.o -O2  # Moderate optimization (default)
build/vyb program.vyb -c program.o -O3  # Aggressive optimization

# Verify object file was created
file hello.o
# Output: hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), with debug_info, not stripped

# Inspect symbols in object file
objdump -t hello.o | grep main
# Output: 0000000000000000 g     F .text  000000000000005c main

Cross-Compilation Support#

Supports LLVM's target set. What is actually exercised today (the rest is backend-available but not covered by any gate):

Target How it is exercised
x86-64 / Linux Release-tested — the canonical JIT suite and the AOT/native-link pass run on every CI build, and the SDK tarball ships this target (docs/sdk/INSTALL.md)
NVPTX (NVIDIA GPUs) Device code emitted and validated with ptxas across sm_75 → sm_90 in CI (sm_90a additionally validates on a local toolchain); silicon-verified on sm_86 (RTX 3090)
AArch64, RISC-V, PowerPC, MIPS, SPARC, WebAssembly, SystemZ, Hexagon, LoongArch, M68k, Xtensa, AVR, MSP430, BPF, AMDGPU, VE, Lanai, XCore Emitted through the same LLVM backend, but not CI-covered — no cross-target fixture or toolchain gate exercises them. Treat these as untested in this tree

Complete Compilation Features:

See: doc/MODULE_FFI_BINARY_ROADMAP.md for the compilation pipeline

1.3 Key Concepts & Terminology#

1.4 Import vs Smuggle - Vyb's Unique Module System#

Vyb introduces a distinctive approach to module imports with two keywords that serve different security and trust models:

import - Trusted, Verified Modules:

smuggle - Flexible, External Sources:

Syntax:

import <module::path> [as <alias>] [from "<locator>"] [;]
smuggle <module::path> [from "<locator>"] [as <alias>] [;]

Locator formats for from:

This dual system provides both safety for production code and flexibility for development:

# Production imports - verified and trusted
import std::io::println
import utils::math::calculate from "./utils"

# Development/experimental - flexible but marked as such
smuggle debug::trace from "github.com/dev/tools"
smuggle experimental::feature from "./local/experiments"

main()<Int> -> {
    println("Production ready!")
    trace("Debug info from smuggled module")
    return calculate(42)
}

Declare dependencies in vyb.toml:

[dependencies]
std = "^1.0.0"  # Signed, from Vyb registry
utils = { git = "https://github.com/user/utils" }  # External, smuggled

This unique import/smuggle distinction makes Vyb's module system both secure and flexible, clearly marking the trust level of your dependencies. See doc/FEATURE_STATUS.md for implementation status.

In This Release#

Vyb v0.7.7 (freedom-1.0 series). Highlights of this cycle:

Feature highlights#

These features are complete and fully tested:

vyb main()<Int> -> { defer println("cleanup done") println("before return") return 0 } // Output: before return \n cleanup done - Math library — abs, min, max, sqrt, sin, cos, tan, exp, log, log2, log10, pow, floor, ceil, round

vyb main()<Int> -> { x<Float> = sqrt(2.0) y<Int> = abs(-5) println_int(y) // 5 return 0 } - String methods — .len(), .contains(), .starts_with(), .ends_with(), .to_upper(), .to_lower(), .substring(), .char_at(), .split(), .format(), String::from_bytes()

vyb main()<Int> -> { s<String> = "Hello, world!" if (s.contains("world")) { println(s.to_upper()) // HELLO, WORLD! } return 0 } - Type inference from initializer — Variables without annotation infer type from RHS

vyb x = 42 // inferred as Int msg = "hello" // inferred as String ok = true // inferred as Bool - Vec for loop type inference — Compiler-generated loop variables require no explicit types

vyb nums<Vec<Int>> = Vec() nums.push(10) for (n in nums) { println(n) // n is automatically Int } - select expressions — Pattern matching that yields a value; Vyb-original concept

vyb grade<String> = select(score) -> { >= 90 -> "A", >= 80 -> "B", >= 70 -> "C", ? -> "F" } - Set patterns in select arms ({ … }) — group several discrete values into one arm so you don't repeat a result per value; the arm matches when the target equals any element. Sets hold literals and bare enum-variant names only; empty {} and mixed types are rejected.

vyb parity(x<Int>)<String> -> { return select(x) -> { {1, 3, 5, 7, 9} -> "odd", {2, 4, 6, 8} -> "even", ? -> "out of bounds" } } - typeof / typename intrinsics — Runtime type introspection

vyb x<Int> = 42 same<Bool> = (typeof(x) == typeof(100)) // true name<String> = typename(x) // "Int" - Immutable bindings (const) — name<Type const> declares an immutable variable

vyb pi<Float const> = 3.14159

✅ Binary Executable Generation#

✅ JSON Serialization & Deserialization#

✅ Native Code Compilation#

✅ Core Language Features#

✅ Advanced Type System#

✅ String Methods#

Comprehensive string manipulation built into the String type:

Method Description Example
.len() Length in bytes s.len() → Int
.contains(sub) Substring test s.contains("Vyb") → Bool
.starts_with(pre) Prefix test s.starts_with("He") → Bool
.ends_with(suf) Suffix test s.ends_with("!") → Bool
.to_upper() Uppercase copy s.to_upper() → String
.to_lower() Lowercase copy s.to_lower() → String
.substring(start, len) Slice s.substring(0, 5) → String
.char_at(i) ASCII code at index s.char_at(1) → Int
.trim() / .strip() Strip surrounding whitespace s.trim() → String
.replace(old, new) Replace all occurrences s.replace("a", "b") → String
.split(sep) Split into Vec<String> of its parts s.split(",") → Vec<String>
.format(args...) Substitute {} placeholders in sequence s.format(42) → String
String::from_bytes(ptr, len) Construct from bytes String::from_bytes(p, n)

String concatenation with + auto-converts non-String operands:

id<Int> = 42
msg<String> = "User ID: " + id    // "User ID: 42"

Splitting returns a fresh Vec<String> of the parts between each occurrence of the separator. An empty separator yields a single-element Vec holding the whole string; leading, trailing, and consecutive separators produce empty parts.

csv<String> = "alpha,beta,gamma"
parts<Vec<String>> = csv.split(",")   // parts.len() == 3; parts.get(1) == "beta"

Formatting substitutes each {} placeholder with the string form of the corresponding argument in order. Arguments of any serializable type (String, Int, Float, Bool, ...) are converted automatically; placeholders beyond the supplied arguments are emitted verbatim.

tmpl<String> = "User {} has {} badge(s)."
msg<String> = tmpl.format("Vyb", 3)   // "User Vyb has 3 badge(s)."

Method calls on value receivers — every String method works not only on a named variable but on any String value: a literal, a function/expression result, or a struct/array field. Only the receiver itself still needs to be a String; no intermediate variable is required.

upper<String> = "hello".to_upper()                        // "HELLO"
head<String> = full_name().substring(0, 5)                // first 5 chars
count<Int> = get_csv().split(",").len()                   // number of fields

✅ Async Programming & Debugging#

Vyb parses and compiles async functions returning Future<T> and the await expression. An async fn(params...)<Future<T>> (T = Int, Float, Bool, String, or Void) starts a task on the stdlib's multi-threaded executor when called — its scalar arguments are snapshotted into a closing environment, and a String result travels back as a heap slot that await hands to the consumer as an owned transfer (a Float travels as its bit pattern and a Bool as 0/1 in the task's result slot). await parks the caller until the task completes (from main) or suspends the current fiber (so a worker can await a child task), including as a bare statement (await f) for Future<Void>. The future is returned by value as a real struct. Owned parameters are snapshotted into the task env: a String keeps its buffer with a retain (+1), an our<T> takes a shared-control-block retain (+1), and a Vec<T> is deep-copied so the env owns an independent copy; the env's per-layout dtor releases each one on cleanup (reclaiming Vec<String> element references too), so they safely outlive the caller's scope while the worker runs.

Async Function Syntax#

// Async function returning Future<Int>
async compute_value()<Future<Int>> -> {
    println("Computing...")
    return 42
}

// Async function that awaits another async function
async process_data()<Future<String>> -> {
    value<Int> = await compute_value()  // runs the pending task to completion
    println("Got value")
    return "processed"
}

Async Key Features#

Usage Example#

main() -> {
    // Call async functions; each returns a Future<T>
    future1<Future<Int>> = compute_value()
    future2<Future<String>> = process_data()
    println("Tasks initiated")
    return
}

See: test/async/async_simple.vyb for a working example.

See also:

✅ Concurrency Modules (stdlib)#

The standard library ships threads (pthread-backed spawn/join/detach, Mutex, Cond, lock-free atomics), channels (thread-safe message passing, chan_select, string channels), tasks (fire-and-forget workers), asyncs (a multi-threaded fiber executor) and agents (mailbox workers). The API and the semantics of each — including what may be captured across a thread boundary — are in Threads and concurrency.

✅ Introspection System#

Vyb features runtime type introspection for self-aware programs:

Type Reflection Operators#

// Get runtime type hash — use for equality comparison
x<Int> = 42
same<Bool> = (typeof(x) == typeof(100))   // true — both Int

// Get type name as String
name<String> = typename(x)                // "Int"

// Works on any expression
same_expr<Bool> = (typeof(x + 5) == typeof(x * 2))   // true

Introspection Key Features#

Usage Examples#

// Type comparison
x<Int> = 42
y<Float> = 3.14
if (typeof(x) != typeof(y)) {
    println("different types")
}

See: test/introspection/ for working examples and doc/INTROSPECTION_DESIGN.md for design philosophy

✅ Aspect System#

Philosophy: Vyb uses aspects + structs instead of classes and inheritance. This provides polymorphism, code reuse, and composition without the complexity and pitfalls of OOP class hierarchies. See doc/TRAIT_SYSTEM_DESIGN.md for detailed design and rationale.

Simple aspect receivers use self; the compiler treats it as the bound Self type. Use explicit receiver types like self<their<Self>> only when ownership mode matters.

Complete Aspect Example#

# Define an aspect (interface): method signatures only, no state
aspect Display {
    show(self)<String> ->
    format(self, prefix<String>)<String> ->
}

# A concrete struct - just data
struct Point {
    x<Int>,
    y<Int>
}

# Bind the aspect to the type, implementing the methods against the data
bind Display -> Point {
    show(self)<String> -> {
        return "Point at (" + self.x + ", " + self.y + ")"
    }

    format(self, prefix<String>)<String> -> {
        return prefix + ": (" + self.x + "," + self.y + ")"
    }
}

# Another type can bind the same aspect
struct Rectangle {
    width<Int>,
    height<Int>
}

bind Display -> Rectangle {
    show(self)<String> -> {
        return "Rectangle " + self.width + "x" + self.height
    }

    format(self, prefix<String>)<String> -> {
        return prefix + ": " + self.width + "x" + self.height
    }
}

main()<Int> -> {
    point<Point> = Point { x = 10, y = 20 }
    rect<Rectangle> = Rectangle { width = 5, height = 7 }

    # Dot-call the bound aspect methods - they read the struct's own fields
    println(point.show())         # "Point at (10, 20)"
    println(point.format("Item")) # "Item: (10,20)"
    println(rect.show())          # "Rectangle 5x7"
    println(rect.format("Item"))  # "Item: 5x7"

    return 0
}

Why Aspects > Classes:

See: doc/ASPECT_SYSTEM_DESIGN.md for complete specification and test/aspect/ for working examples

Core Contracts (core::aspects)#

The standard library ships six canonical contract aspects — Display, Debug, Clone, Equatable, Hashable, and Comparable (with Comparable : Equatable) — bound to the primitive scalar types (Int/Float/Bool/Char/String). core::aspects is auto-imported into non-stdlib modules (opt out with no_core()), so contract methods are available on built-in scalars with no import. They bind to user structs the same way, and drive the generic call sites for the stdlib collections and Iterator protocol:

// Hash/comparison-backed collections (import collections) resolve the
// Hashable/Comparable bounds on generic keys automatically.
nums<BTreeMap<Int, String>> = BTreeMap<Int, String>()

// Unqualified bounded-type-parameter dispatch resolves through the bound.
show_all<T<Display>>(item<T>) -> {
    println(item.display())   // resolves via the bound Display aspect
}

Primitive Types#

Signed Integers:

Type Description Size Range/Notes Example
Int / Int64 Signed integer (default) 64-bit -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 x<Int> = 42
Int32 32-bit signed integer 32-bit -2,147,483,648 to 2,147,483,647 count<Int32> = 100
Int16 16-bit signed integer 16-bit -32,768 to 32,767 small<Int16> = 1000
Int8 8-bit signed integer 8-bit -128 to 127 byte<Int8> = 127

Unsigned Integers:

Type Description Size Range/Notes Example
UInt64 64-bit unsigned integer 64-bit 0 to 18,446,744,073,709,551,615 max<UInt64> = 18446744073709551615u
UInt32 32-bit unsigned integer 32-bit 0 to 4,294,967,295 count<UInt32> = 100
UInt16 16-bit unsigned integer 16-bit 0 to 65,535 port<UInt16> = 8080
UInt8 8-bit unsigned integer 8-bit 0 to 255 byte<UInt8> = 255

Floating Point:

Type Description Size Precision Example
Float / Float64 Double precision (default) 64-bit IEEE 754 double (~15-17 digits) pi<Float> = 3.14159
Float32 Single precision 32-bit IEEE 754 single (~6-9 digits) ratio<Float32> = 1.5

Character Types:

Type Description Size Range/Notes Example
Char UTF-8 code unit 8-bit Single byte (0-255) ch<Char> = 65 # 'A'
Rune Unicode code point 32-bit Full Unicode range (U+0000 to U+10FFFF) emoji<Rune> = 128512 # 😀

Other Types:

Type Description Size Range/Notes Example
Bool Boolean 1-bit true or false flag<Bool> = true
String UTF-8 string Variable Heap-allocated fat pointer { ptr, len } name<String> = "Alice"
Bytes Raw binary data Variable Fat pointer for byte sequences { ptr, len } Future byte literals
Void No value 0-bit Used for functions that don't return print() -> { ... }

Collection Types#

Type Description Mutability Example
[T; N] Fixed-size array Mutable elements nums<[Int; 5]> = [1, 2, 3, 4, 5]
Vec<T> Dynamic array Mutable elements items<Vec<String>> = Vec()
HashMap<K,V> Hash-bucket map (import collections) Mutable m<HashMap<String, Int>> = HashMap<String, Int>()
HashSet<K> Distinct-key set (import collections) Mutable s<HashSet<String>> = HashSet<String>()
BTreeMap<K,V> Key-ordered map (import collections) Mutable b<BTreeMap<Int, String>> = BTreeMap<Int, String>()
Tuple<T,U,...> Heterogeneous tuple (variadic) Immutable data<Tuple<Int,String,Bool>>

Ownership Types#

Type Description Use Case Example
my<T> Unique ownership Single owner, move semantics data<my<Person>> = my(Person{...})
our<T> Shared ownership Reference counting config<our<Settings>> = our(Settings{...})
their<T> Borrowed reference Non-owning access ref<their<Data>> = view(owner)
loc<T> Raw pointer Freedom operations only ptr<loc<Int>> = loc(variable)

✅ Canonical Ownership Syntax#

Vyb features unified canonical syntax for ownership and borrowing operations:

Type Annotations#

# In variable declarations and function signatures
data<my<String>>     # Unique ownership type
shared<our<Config>>  # Shared ownership type
view<their<Data>>    # Borrowed reference type

Value Construction#

# Create owned values with my() and our() constructors
unique<my<String>>   = my("owned string")
shared<our<Config>>  = our(Config::new())
result<my<Data>>     = my(compute_data())

Borrowing Operations#

# Create temporary references with view/borrow/soft functions
readonly<their<String const>> = view(data)     # Immutable borrow
writable<their<String>>       = borrow(data)   # Mutable borrow
weak<mild<Node>>              = soft(shared)   # Weak reference
length<Int>                   = view(data).len()
borrow(data).clear()

Legacy Syntax Migration#

All legacy make_my(), make_our(), and prefix view expr / borrow expr syntax can be migrated to canonical syntax using the migration tool:

# Scan for legacy syntax
build/vyb migrate_syntax.vyb --scan --directory .

# Apply migrations with backup
build/vyb migrate_syntax.vyb --migrate --directory . --backup --report

Migration Results: ✅ 346 syntax updates applied across 22 files, ensuring consistent canonical syntax throughout the entire codebase.

✅ Memory Management#

Reclaiming Temporaries#

A temporary is unwound exactly like a named binding of the same type, so what is not reclaimed is a temporary whose own result aliases its storage: an element accessor such as payload.split("\n").get(0) returns a value that may point into the temporary's element buffer, so that buffer is left alone (one buffer per evaluation leaks). Bind the Vec to a name first — parts<Vec<String>> = payload.split("\n"), then parts.get(0) — and the binding's own scope-exit cleanup releases it.

main()<Int> -> {
    payload<String> = "a|b|c\nx|y|z\n"
    total<Int> = 0

    # The temporary Vec from split() is released as len() returns
    total = total + payload.split("\n").len()

    # A named binding owns its buffer, so a borrow out of it is safe
    parts<Vec<String>> = payload.split("\n")
    println(total.to_string() + " " + parts.get(0))
    return 0
}

test/ownership/vec_temp_receiver_reclaimed.vyb and test/ownership/vec_temp_{push_arg,set_arg,discarded_call}_reclaimed.vyb lock the reclaimed shapes in (the push/set fixtures read their rows back, so a slot that aliased freed storage would show as garbage).

Weak References with mild#

Vyb introduces mild<T> - weak references that solve circular reference problems without preventing cleanup:

Why mild?

Key Methods:

# grab() -> our<T>?
# Attempts to upgrade mild reference to strong reference
# Returns nil if target has been destroyed
parent_strong<our<TreeNode>> = node.parent.grab()

# released() -> Bool
# Check if target object has been freed
# Returns true if destroyed, false if still alive
is_alive<Bool> = !node.parent.released()

Creating Mild References:

# Use soft() to create mild<T> from our<T>
shared_data<our<Config>> = our(Config::new())
shadow<mild<Config>> = soft(shared_data)  # Create mild reference

Example: Tree with Parent Pointers

struct TreeNode {
    value<Int>,
    children<Vec<our<TreeNode>>>,  # Strong refs to children
    parent<mild<TreeNode>>          # Mild ref to parent (breaks cycle)
}

create_child(parent<our<TreeNode>>, value<Int>)<our<TreeNode>> -> {
    return our(TreeNode {
        value: value,
        children: Vec(),
        parent: soft(parent)  # Create weak reference with soft()
    })
}

access_parent(node<our<TreeNode>>)<Int> -> {
    # Safely access parent through weak reference
    if (parent<our<TreeNode>> = node.parent.grab()) {
        return parent.value  # Success - parent still alive
    } else {
        return -1  # Parent was destroyed
    }
}

See: doc/OWNERSHIP_MILD.md for complete documentation and test/ownership/mild_test.vyb for examples

✅ Developer Experience#

✅ Native Bridge / FFI#

Vyb's native bridge to C libraries is shipped and production-usable:

Layer Status Description
Vyb Runtime ✅ Complete runtime/vyb_runtime.c — ownership-tracked Vec/String/Math/I/O (our<T> is reference-counted) all linked automatically
LLVM intrinsics ✅ Complete malloc, free, memset, printf-style print all registered in JIT
C stdlib (math) ✅ Complete libm linked; sqrt, sin, cos, pow, etc. all working
C stdlib (I/O) ✅ Complete libc linked; I/O built on top of C runtime
extern "C" blocks ✅ Complete Declare C functions callable from Vyb (incl. variadic printf(fmt, ...)); maps loc<T>→T* and C aliases (CInt, CString, CSize, …)
#[repr(C)] structs ✅ Complete Force C-compatible struct layout for FFI
vyb bindgen (MVP + libclang) ✅ vyb bindgen <header.h> emits importable extern/repr(C)/enum bindings from a C subset; vyb bindgen <header.h> --full adds the libclang full-preprocessor backend (#include expansion, conditional evaluation, expression + function-like macros)

With extern "C" and the typed C aliases, the POSIX API is reachable with a thin Vyb wrapper, enabling networking, file I/O, threading, and more without language-level changes. FFI callbacks arrive as bare loc<fn> function pointer parameters (see test/ffi/callback_fnptr.vyb), and variadic C functions work directly (printf("%s", s) accepts a Vyb String). See doc/FFI_DESIGN.md for the complete design.

Language Overview#

Vyb (freedom-1.0 series) is a mature, actively developed systems programming language with name-first syntax, a sized type system (Int8–Int64, UInt8–UInt64, Float32/Float64, Char, Rune, Bytes), compile-time monomorphized generics, aspect/bind polymorphism, pattern matching, Vec<T>, and comprehensive collection support. The core language is stable and well tested.

Closures and lambdas#

A lambda is a first-class value of type fn(...) -> R: it can be held in a variable, passed to a function, returned from one, and fed to combinators such as Vec::map / filter / fold. The body may be an expression or a block.

makeAdder(base<Int>)<fn(Int) -> Int> -> {
    return |x<Int>| -> x + base
}

main()<Int> -> {
    twice<Int> = 2
    f<fn(Int) -> Int> = |x<Int>| -> x * twice
    println(f(10).to_string())            // 20 — a copy of `twice`

    add5<fn(Int) -> Int> = makeAdder(5)
    println(add5(10).to_string())         // 15 — the environment outlives the frame
    return 0
}

Value shape. A lambda that captures nothing lowers to a bare function pointer (null environment), so it satisfies an fn parameter with no allocation. A capturing lambda is a {env, fn} pair whose environment is a reference-counted heap struct holding exactly the captured values; the callable is an ordinary function whose hidden first parameter is that environment, so both forms call through one shape.

Capture forms and what each means.

Capture Written as Semantics
by value a plain name the environment holds a copy; later writes to the outer variable do not reach the closure
move capturing a my<T> ownership transfers into the closure; the outer variable is dead afterwards (use-after-move)
shared capturing an our<T> the environment takes a strong reference, so the target stays alive for the life of the closure
mutable assigning to a captured name the environment stores the address of the outer variable, so writes inside the closure write through to the enclosing scope. If the closure can outlive its frame (returned, stored in a field or a Vec, handed to a thread) that address is replaced by a heap cell the environment owns, initialised from the binding
counter<Int> = 0
bump<fn(Int) -> Int> = |u<Int>| -> counter = counter + 1
bump(0)
bump(0)
println(counter.to_string())          // 2 — the outer variable sees both writes

shared<our<Int>> = our(7)
read<fn(Int) -> Int> = |u<Int>| -> shared + 3
println(read(0).to_string())          // 10 — the closure retains the shared value

Lifecycle. The environment is reference-counted: copying the closure retains it, exiting the scope or overwriting the variable releases it, and a closure returned from a function hands back an owned reference, so the environment survives the frame that built it.

Escaping mutable captures are boxed. A mutable capture stores the outer variable's address, which is only meaningful while that frame is alive. When the analyzer sees a closure value leave its frame — returned, stored into a struct field or a Vec element, or handed to thread_spawn/task_spawn — each mutable capture is instead boxed: codegen allocates a heap cell, initialises it with the binding's current value (deep-copying storage the cell will own), and stores the cell pointer in the environment. The environment owns the cell — its generated destructor reclaims the cell's payload, if any, then frees the block when the last closure reference drops — so the closure works after its defining frame has exited:

makeCounter()<fn() -> Int> -> {
    n<Int> = 5
    return || -> {
        n = n + 1
        return n
    }
}

main()<Int> -> {
    f<fn() -> Int> = makeCounter()
    println(f().to_string())              // 6 — read from the cell, not from the dead frame
    println(f().to_string())              // 7 — the next call starts from the cell's value
    return 0
}

A closure that does not escape keeps the stack path, so the write-back contract in the table above is unchanged: two closures over one binding still do not share mutations through a read, and the enclosing frame still observes them.

A closure that does escape promotes its captured binding to one refcounted cell shared with the defining frame (checkpoint (c)), so the frame and every other closure over that binding observe the mutation — a spawner sees the write its spawned thread made through the closure, and two escaping closures over one binding see each other's (test/lambda/test_closure_mutable_frame_observes.vyb, test_closure_mutable_shared_binding.vyb). The frame holds one reference on the cell and each capturing environment another; the last owner runs the cell's destructor, which reclaims the payload and frees the block.

An in-place mutation through a mutable capture is a write like any other — v.push(x), v.set(i, x), v[i] = x, v.clear() on a captured binding are recorded as writes to it, and each exit from the closure body flushes the captured value back, so the enclosing scope and later invocations observe the mutation (test/lambda/test_closure_mutable_capture_inplace_mutation.vyb).

Where it stops. A mutable capture whose declared type has no sound heap-cell representation is still refused when the closure is returned — a borrow (their<T>/loc<T>), an FFI handle, an optional, an array, or a name the analysis cannot resolve (a type parameter):

Semantic Errors:
  Cannot return a closure with mutable captures: capture 'ro' holds state that would alias the enclosing stack frame after this function returns, and a mutable capture of that type cannot be boxed safely. Capture it by value, share it as our(ro), or keep the closure inside the frame.

Moving a my<T> into a closure is destructive in the same way, and reading the variable afterwards is an error:

Semantic Errors:
  Use after move: 'x' has been moved and is no longer valid.

Generic lambdas are still an open item (TODO.md §3), and the per-binding shared cell — which would let the defining frame observe a mutation made through an escaping closure — is not implemented; an escaping closure's writes land in its own cell.

Threads and concurrency#

Vyb's concurrency is a small set of stdlib modules over POSIX threads, plus one compile-time rule about closures: a closure handed to another thread must be able to cross an OS thread boundary.

The modules.

What a closure may capture across a thread boundary. thread_spawn, task_spawn, async_spawn and agent_start hand a closure's environment to another OS thread, so every captured value must remain valid there. The compiler proves this structurally from the type graph; the capability is called handoff:

import threads::{thread_spawn, thread_join}

struct Pair { a: Int, b: Int }

main()<Int> -> {
    local<Int> = 3
    shared<our<Int>> = our(4)
    p<Pair> = Pair { a = 1, b = 2 }

    h<Int> = thread_spawn(|| -> local + shared + p.a + p.b) else -1
    if (h < 0) { return 1 }
    println("joined = " + thread_join(h).to_string())   // 10
    return 0
}

A capture whose declared type still names a type parameter is recorded by the semantic pass and judged by codegen at the instantiated type, so the same generic function can be accepted at one instantiation and refused at another. The check runs before code is generated, so a refusal is a compile error, not a runtime surprise:

Semantic Errors:
  thread_spawn: 'h' is Holder -- it cannot cross a thread boundary (not handoff-capable). Hand it off as shared ownership (our(h)) or pass a copy.

When the derivation is not enough: the reviewed claim (Handoff / Viewable). Ordinary types need nothing from you — their capability is derived from the type graph as described above. For the shapes the derivation cannot see through, core::aspects declares two marker aspects (re-exported by core::prelude), and a bind states the claim:

aspect Handoff  { handoff(self)<Bool>  -> { return true } }
aspect Viewable { viewable(self)<Bool> -> { return true } }

A marker aspect has no contract to satisfy; bind Handoff -> T is simply the claim "a value of T may cross a thread boundary", reviewed by whoever wrote the bind. This is a general escape hatch, not an FFI-only feature. Reach for it whenever the structural verdict is blind to your invariants or simply wrong for your design:

Here is a non-FFI claim — a struct holding a unique owner, which the structural derivation refuses on its own, handed to a worker that takes the owner with it:

import core::aspects
import threads::{thread_spawn, thread_join}

struct Token { n: my<Int> }

bind Handoff -> Token {
    handoff(self<Token>)<Bool> -> { return true }   // the claim; reviewed, not proven
}

main()<Int> -> {
    t<Token> = Token { n = my(7) }
    h<Int> = thread_spawn(|| -> {
        seen<Token> = t          // the unique owner moves into the closure environment
        return 0
    }) else -1
    if (h < 0) { return 1 }
    r<Int> = thread_join(h) else -2
    println("joined = " + r.to_string())                 // joined = 0
    return 0
}

And the FFI-shaped claim — a handle carrying raw memory, which the closure reads on the other thread (freedom is where raw addresses live):

import core::aspects
import threads::{thread_spawn, thread_join}

struct Handle { raw: loc<Int> }

bind Handoff -> Handle {
    handoff(self<Handle>)<Bool> -> { return true }
}

main()<Int> -> {
    x<Int> = 41
    h<Int> = -1
    freedom {
        p<loc<Int>> = loc(x)
        hd<Handle> = Handle { raw = p }
        h = thread_spawn(|| -> {
            seen<Handle> = hd
            return 0
        }) else -1
    }
    if (h < 0) { return 1 }
    r<Int> = thread_join(h) else -2
    println("joined = " + r.to_string() + " x = " + x.to_string())
    return 0                                             // joined = 0 x = 41
}

How the claim behaves:

warning: curated thread-boundary bind: `bind Handoff -> Token` claims the type may cross a thread boundary, but its structural shape is not handoff-capable -- the explicit bind wins, so the program is accepted; the payload then carries the programmer's guarantee (doc/THREAD_BOUNDARY_SCOPE.md).
import core::aspects
import threads::{thread_spawn, thread_join}

struct Window { raw: loc<Int> }

bind Viewable -> Window {                 // the weaker, read-only claim
    viewable(self<Window>)<Bool> -> { return true }
}

main()<Int> -> {
    x<Int> = 41
    freedom {
        p<loc<Int>> = loc(x)
        w<Window> = Window { raw = p }
        h<Int> = thread_spawn(|| -> {     // still refused: viewable is not a spawn licence
            seen<Window> = w
            return 0
        }) else -1
        if (h >= 0) { thread_join(h) }
    }
    return 0
}
warning: curated thread-boundary bind: `bind Viewable -> Window` claims the type may cross a thread boundary, but its structural shape is not viewable -- the explicit bind wins, so the program is accepted; the payload then carries the programmer's guarantee (doc/THREAD_BOUNDARY_SCOPE.md).

Semantic Errors:
  thread_spawn: 'w' is Window -- it cannot cross a thread boundary (not handoff-capable). Hand it off as shared ownership (our(w)) or pass a copy.

The claim travels with the type, so the same curated Token also crosses as a shared owner — the shape you reach for when several threads read it (runs; prints joined = 0):

import core::aspects
import threads::{thread_spawn, thread_join}

struct Token { n: my<Int> }

bind Handoff -> Token {
    handoff(self<Token>)<Bool> -> { return true }
}

main()<Int> -> {
    shared<our<Token>> = our(Token { n = my(7) })   // a shared owner of a curated type
    h<Int> = thread_spawn(|| -> {
        seen<our<Token>> = shared                   // `our<Token>` inherits the claim on `Token`
        return 0
    }) else -1
    if (h < 0) { return 1 }
    r<Int> = thread_join(h) else -2
    println("joined = " + r.to_string())            // joined = 0
    return 0
}

The full rule set and the measurements behind it are in doc/THREAD_BOUNDARY_SCOPE.md; the authoritative manual is docs/refman/PROGRAMMERS_GUIDE.md §5.

Channels are the usual way to get values out of a thread, while a closure's captures carry values in:

import threads::{thread_spawn, thread_join}
import channels::{chan_new, chan_send, chan_recv}

main()<Int> -> {
    ch<Int> = chan_new() else 0
    h<Int> = thread_spawn(|| -> {
        i<Int> = 0
        while (i < 3) {
            chan_send(ch, i * 10)
            i = i + 1
        }
        chan_send(ch, -1)
        return 0
    }) else -1
    if (h < 0) { return 1 }

    total<Int> = 0
    closer<Bool> = false
    while (!closer) {
        v<Int> = chan_recv(ch) else -1
        if (v < 0) { closer = true } else { total = total + v }
    }
    thread_join(h)
    println("total = " + total.to_string())   // 30
    return 0
}

The executor and mailbox forms:

import asyncs::{async_spawn, async_await, async_sleep_ms, async_run_all}
import tasks::{task_spawn, task_await, task_free}

a<Int> = async_spawn(|| -> { async_sleep_ms(20); return 10 }) else 0
b<Int> = async_spawn(|| -> { async_sleep_ms(20); return 32 }) else 0
println("async: " + async_await(a).to_string() + ", " + async_await(b).to_string())  // async: 10, 32

t<Int> = task_spawn(|| -> 40 + 2) else 0
println("task: " + task_await(t).to_string())   // task: 42
task_free(t)
async_run_all()
import agents

counter<Int> = agent_start(|message<Int?>| -> {
    match (message) {
        v -> { println("agent got " + v.to_string()) }
        ? -> { println("agent: mailbox closed") }
    }
}) else -1

agent_send(counter, 42)
agent_send(counter, 43)
agent_close(counter)
agent_free(counter)

Language Features Showcase#

// Complete language demonstration showing all major features

// Modern struct syntax with typed fields
struct Person {
    name<String>,
    age<Int>,
    scores<Vec<Int>>
}

// Define aspect for gradeable entities
aspect Gradeable {
    letter_grade(self)<String> -> { }
    is_passing(self)<Bool> -> { }
}

// Bind aspect to Person using match in the implementation
bind Gradeable -> Person {
    letter_grade(self)<String> -> {
        // Calculate average score
        total<Int> = 0
        i<Int> = 0
        while (i < self.scores.len()) {
            total = total + self.scores.get(i)
            i = i + 1
        }
        avg<Int> = total / self.scores.len()

        // Match statement with comparison patterns for grade ranges
        match (avg) {
            >= 90 -> return "A",
            >= 80 -> return "B",
            >= 70 -> return "C",
            >= 60 -> return "D",
            ? -> return "F"
        }
    }

    is_passing(self)<Bool> -> {
        grade<String> = self.letter_grade()
        match (grade) {
            "F" -> return false,
            ? -> return true
        }
    }
}

// select expression — Vyb-original: pattern matching that returns a value
get_grade_description(score<Int>)<String> -> {
    grade<String> = select(score) -> {
        >= 90 -> "Excellent",
        >= 80 -> "Good",
        >= 70 -> "Satisfactory",
        >= 60 -> "Passing",
        ? -> "Needs Improvement"
    }
    println(grade)
    return grade
}

// Generic function using aspect bounds
print_student_status<T<Gradeable>>(student<T>) -> {
    grade<String> = student.letter_grade()
    passing<Bool> = student.is_passing()
    println("Grade: " + grade)
    if (passing) {
        println("Status: PASSING")
    } else {
        println("Status: FAILING")
    }
}

// Resizable collections with full method support
create_person(name<String>, age<Int>)<Person> -> {
    person<Person> = Person {
        name = name,
        age = age,
        scores = Vec()
    }
    person.scores.push(85)
    person.scores.push(92)
    person.scores.push(78)
    return person
}

main()<Int> -> {
    student<Person> = create_person("Alice", 20)

    // Defer for guaranteed cleanup
    defer println("done")

    // Type introspection
    same<Bool> = (typeof(student.age) == typeof(42))

    // Call aspect method through generic function
    print_student_status(student)

    // Use select with comparison patterns
    description<String> = get_grade_description(85)

    return 0
}

Memory Safety & Freedom Operations#

Vyb's design philosophy: FREEDOM over restrictions. The language provides compiler-managed ownership by default, but empowers programmers with low-level control when needed:

// Ownership type syntax (runtime enforcement is in place for our/mild/their;
// full my<T> move tracking is still in progress)
restricted_memory_example()<Int> -> {
    owned<my<String>> = my("unique data")         // Unique ownership
    shared<our<String>> = our("shared data")      // Shared, ref-counted
    another_ref<our<String>> = shared             // Reference count +1
    view_ref<their<String const>> = view(shared)  // Immutable borrow
    return 42
}

// freedom block — low-level pointer access
freedom_memory_example()<Int> -> {
    x<Int> = 42
    freedom {
        p<loc<Int>> = loc(x)       // Get pointer to x
        println("in freedom block")
    }
    return x
}

FREEDOM Code Guidelines:

  1. Minimize the scope of freedom blocks
  2. Document all invariants and assumptions
  3. Validate pointers before dereferencing
  4. Encapsulate freedom operations behind restricted abstractions
  5. Test freedom code extensively

Syntax and Literals#

Declaration bodies are braced (name(params)<ReturnType> -> { ... }) and the unified name<Type> form gives every declaration the same shape. Statements are newline-separated inside the braces.

Literal Forms#

// Integer literals
x<Int> = 42                       // 64-bit signed integer (default)
large<Int> = -9223372036854775808  // Full 64-bit range
red<Int> = 0xFF                   // Hexadecimal (base 16)
mask<Int> = 0b11001110            // Binary (base 2)
ud<UInt32> = 255u                 // Unsigned literal suffix
maxU<UInt64> = 18446744073709551615u  // Full UInt64 range (2^64 - 1)

// Sized integers use the same literal forms (0x80 means 128 as a byte).
b<UInt8> = 0x80

// Floating point literals
pi<Float> = 3.14159               // 64-bit IEEE 754 double precision
e<Float> = 2.71828

// Boolean literals
flag<Bool> = true
active<Bool> = false

// String literals
name<String> = "Alice"            // UTF-8 string
greeting<String> = "Hello\nVyb"  // Escape sequences supported
empty<String> = ""

// Array literals (fixed-size)
numbers<[Int; 5]> = [1, 2, 3, 4, 5]
floats<[Float; 3]> = [1.0, 2.5, -3.14]

// Dynamic arrays (Vec<T>)
items<Vec<Int>> = Vec()

// Immutable binding
PI<Float const> = 3.14159

Syntax Examples#

// Variable declarations with unified syntax
x<Int> = 42                  // Mutable variable
PI<Float const> = 3.14159    // Immutable constant (name<Type const>)

// Function declarations — name comes first
add(a<Int>, b<Int>)<Int> -> a + b

// Struct definitions with typed fields
struct Point {
    x<Float>,
    y<Float>
}

// Pattern matching with comparison operators and wildcard ?
process_value(val<Int>)<String> -> {
    match (val) {
        0 -> "zero",
        >= 1 -> "positive",
        ? -> "negative"
    }
}

// select — pattern matching that returns a value (Vyb-original)
classify(score<Int>)<String> -> {
    return select(score) -> {
        >= 90 -> "A",
        >= 80 -> "B",
        ? -> "C-or-below"
    }
}

// defer — LIFO scope-exit cleanup
open_and_process()<Int> -> {
    defer println("cleanup")
    println("working")
    return 0
}

Type System Features#

Current Primitive Types:

Current Collection Types:

HashMap, HashSet, and BTreeMap ship in stdlib/collections and are imported with import collections. Vec<T> itself layers Vyb-written higher-order helpers there — map / filter / reduce, sorted / reversed / find, min / max, and sort_in_place — taking non-capturing lambdas (fn) where a mapping function is needed.

Ownership Types: my<T> (unique), our<T> (shared), their<T> (borrowed), mild<T> (mild reference), loc<T> (freedom raw pointer)

Type Aliasing: All numeric types support multiple naming conventions:

Basic Syntax#

Vyb uses clean, expressive syntax with flexible parameter syntax:

# Functions support both standard and shorthand parameter syntax

# Standard syntax: explicit parameter<Type> or parameter<Type const>
add_standard(a<Int>, b<Int const>)<Int> -> {
    return a + b
}

# Shorthand syntax: Type directly (implicitly mutable)
add_shorthand(Int a, Int b)<Int> -> {
    return a + b
}

# Mixed syntax: combining both forms in same function
mixed_params(x<Int>, Int y, const Int z)<Int> -> {
    return x + y + z
}

# Complex return types with auto-serialization
get_data()<Int, String, Bool> -> {
    return 42, "result", true
}
# When called from main(), outputs: {"Int":42,"String":"result","Bool":true}

Function Parameters#

Vyb supports two parameter syntax styles for maximum flexibility:

# Standard syntax: explicit mutability
format_standard(prefix<String>, value<Int const>)<String> -> {
    return prefix + String::from_int(value)
}

# Shorthand syntax: type-first (more concise)
format_shorthand(String prefix, const Int value)<String> -> {
    return prefix + String::from_int(value)
}

# Both produce identical behavior and LLVM IR

Variables and Types#

# Variables with type inference
x = 42          # Int
name = "Alice"  # String
flag = true     # Bool

# Explicit typing
count<Int> = 0
message<String> = "Hello"

# Immutable values (const is a type modifier)
PI<Float const> = 3.14159
MAX_SIZE<Int const> = 1000

Structs and Data#

# Define a struct with modern syntax
struct Point {
    x<Int>,
    y<Int>
}

# Create and use with field access
main()<Int> -> {
    p<Point> = Point { x = 10, y = 20 }
    return p.x + p.y  # Returns 30
}

Modules & Import System#

Vyb's unique dual import system provides both security and flexibility:

# Trusted imports from verified sources
import std::collections::Vec
import utils::math::calculate
import std::io::println

# Flexible imports for development and experimentation
smuggle debug::trace from "github.com/dev/tools"
smuggle experimental::parser from "./local/experiments"

# Use imported symbols
main()<Int> -> {
    numbers<Vec<Int>> = Vec()
    numbers.push(calculate(10))

    println(numbers)  # From trusted std::io
    trace("Debug info")  # From smuggled debug module

    return numbers.get(0)
}

Import Types:

Declare dependencies in vyb.toml:

[dependencies]
std = "^1.0.0"                    # Verified registry package
utils = { git = "https://..." }    # External Git repository
local_tools = { path = "../tools" } # Local development dependency

Bundle & Sharing System: Fine-grained visibility control with bundles (bundle(...) and share(...) directives; see test/modules/import_bundle_allowed.vyb and test/modules/import_private_rejected.vyb):

# Declare module bundles
bundle(math, math.Core)

# Control symbol visibility
share(all) public_function()<Int> -> { ... }      # Public to all
share(math.UI) ui_helper()<Int> -> { ... }        # Only to math.UI bundle
internal_helper()<Int> -> { ... }                 # Private to this file

Arrays and Collections#

# Resizable vectors with full method support
main()<Int> -> {
    numbers<Vec<Int>> = Vec()
    numbers.push(10)
    numbers.push(20)
    numbers.push(30)

    println(numbers)  # Outputs: { "type": "Vec<Int>", "address": "0x..." }

    length<Int> = numbers.len()    # Gets 3
    first<Int> = numbers.get(0)    # Gets 10
    last<Int> = numbers.pop()      # Gets and removes 30

    return first + last  # Returns 40
}

# Vec iteration - parentheses mandatory
iterate_example()<Int> -> {
    items<Vec<Int>> = Vec()
    items.push(1)
    items.push(2)
    items.push(3)

    sum<Int> = 0
    for (item in items) {
        sum = sum + item
    }

    return sum  # Returns 6
}

# Fixed-size arrays also supported
array_example()<Int> -> {
    fixed<[Int; 3]> = [1, 2, 3]
    return fixed[0]  # Array indexing
}

Keyed & Set Collections — imported with import collections:

import collections

main()<Int> -> {
    # HashMap<K, V> — hash-bucket index, auto-grows, average O(1) lookup
    scores<HashMap<String, Int>> = HashMap<String, Int>()
    scores.put("alpha", 1)
    scores.put("beta", 2)
    scores.put("alpha", 99)               # overwrites the existing key
    n_scores<Int> = scores.size()         # 2
    score<Int?> = scores.get("beta")                 # Int? (2 when present, absent if missing)
    has_alpha<Bool> = scores.contains_key("alpha")   # true

    # HashSet<K> — distinct keys
    tags<HashSet<String>> = HashSet<String>()
    tags.insert("x")
    tags.insert("y")
    again<Bool> = tags.insert("x")        # false (already present)

    # BTreeMap<K, V> — keys iterate in sorted (Comparable) order
    ordered<BTreeMap<Int, String>> = BTreeMap<Int, String>()
    ordered.put(3, "three")
    ordered.put(1, "one")
    ordered.put(2, "two")                 # iteration order 1, 2, 3
    return 0
}

Higher-Order Vec Combinators — non-capturing lambdas, also import collections:

import collections

main()<Int> -> {
    v<Vec<Int>> = Vec()
    v.push(3); v.push(9); v.push(5)

    doubled<Vec<Int>> = v.map(|n<Int>| -> n * 2)          # fresh [6, 18, 10]
    odds<Vec<Int>>    = v.filter(|n<Int>| -> n % 2 == 1)  # fresh [3, 9, 5]
    total<Int>        = v.reduce(0, |a<Int>, b<Int>| -> a + b)  # 17
    v.sort_in_place()                                     # in place: [3, 5, 9]
    smallest<Int> = v.first()                             # 3
    return 0
}

Control Flow#

# Conditional expressions with modern syntax
check_sign(x<Int>)<String> -> {
    if (x > 0) {
        return "positive"
    } else if (x < 0) {
        return "negative"
    } else {
        return "zero"
    }
}

# While loops with break/continue
factorial(n<Int>)<Int> -> {
    result<Int> = 1
    i<Int> = 1
    while (i <= n) {
        if (i == 0) {
            continue
        }
        result = result * i
        i = i + 1
        if (result > 1000) {
            break
        }
    }
    return result
}

# Vec iteration with for loops (parentheses mandatory)
sum_vector(numbers<Vec<Int>>)<Int> -> {
    total<Int> = 0
    for (num in numbers) {
        if (num < 0) {
            continue  # Skip negative numbers
        }
        total = total + num
        if (total > 100) {
            break  # Stop if sum exceeds 100
        }
    }
    return total
}

# For loops over the Iterator protocol: any non-identifier iterable
# expression desugars onto `.next()` (e.g. `v.iter()`, or a custom
# `bind Iterator` type). `break`/`continue` work and re-enter `next()`.
# An optional step (`for (x in v.iter(), 2)`) yields every 2nd element.
# The range (`0..n`) and plain-Vec (`for (x in vec)`) forms above are unchanged.
for (x in numbers.iter()) {
    total = total + x
}

# Range-based for loops (inclusive)
count_to_ten()<Int> -> {
    sum<Int> = 0
    for (i in 0..10) {
        sum = sum + i
    }
    return sum  # Returns 55
}

# Pattern matching with match statements
describe_number(x<Int>)<String> -> {
    match (x) {
        0 -> return "zero",
        1 -> return "one",
        42 -> return "the answer",
        ? -> return "some number"
    }
}

# Match arms can execute any statement, including return
# The match statement itself doesn't return a value, but pattern arms can
# return from the enclosing function when the type matches

# Select expressions - elegant pattern matching that returns a value
compute_bonus(level<Int>)<Int> -> {
    # Simple select with naked expressions (auto-return)
    multiplier<Int> = select(level) -> {
        1 -> 10,
        2 -> 20,
        3 -> 30,
        ? -> 5
    };
    return multiplier * 100
}

# Select with complex blocks and explicit pass keyword
process_request(code<Int>)<Int> -> {
    result<Int> = select(code) -> {
        1 -> {
            temp<Int> = 10;
            pass temp
        },
        2 -> {
            x<Int> = 20;
            pass x
        },
        3 -> {
            msg<Int> = 300;
            println(msg);
            pass msg
        },
        ? -> {
            pass 0
        }
    };
    return result
}

# Select expressions combine the best of match and expression values:
# - Naked expressions (1 -> 10) auto-return without 'pass'
# - Complex blocks ({ ... }) use 'pass' keyword to return value
# - 'pass' returns from the block, NOT the enclosing function
# - Type inference from first case ensures type safety
# - Wildcard '?' provides default case handling

select may also be used as a bare statement for side effects only — arms need not yield a value, and a block arm without pass simply falls through to the following statements. And because a select arm body is itself an expression, a select may appear as an arm of an enclosing select for per-branch sub-selection (see test/new_features/test_select_statement.vyb and test/new_features/test_select_nested.vyb).

Comparison Patterns (Range Matching)#

Vyb's pattern matching supports comparison patterns for elegant range-based matching with compile-time safety:

# Comparison operators in patterns: >, <, >=, <=, ==, !=

# Select expression - returns a value directly
classify_score(score<Int>)<String> -> {
    return select(score) -> {
        >= 90 -> "A",
        >= 80 -> "B",
        >= 70 -> "C",
        >= 60 -> "D",
        ? -> "F"
    }
}

# Match statement - returns from enclosing function
classify_score_match(score<Int>)<String> -> {
    match (score) {
        >= 90 -> return "A",
        >= 80 -> return "B",
        >= 70 -> return "C",
        >= 60 -> return "D",
        ? -> return "F"
    }
}

# Tax rate calculation with select
compute_tax_rate(income<Int>)<Float> -> {
    return select(income) -> {
        < 10000 -> 0.0,
        < 50000 -> 0.15,
        < 100000 -> 0.25,
        ? -> 0.35
    }
}

# Comparison patterns work with floats and integers
categorize_temperature(temp<Float>)<String> -> {
    match (temp) {
        < 0.0 -> return "freezing",
        < 10.0 -> return "cold",
        < 20.0 -> return "mild",
        < 30.0 -> return "warm",
        ? -> return "hot"
    }
}

# Unreachable pattern detection - compiler ERROR
invalid_patterns(x<Int>)<String> -> {
    match (x) {
        > 10 -> return "big",
        > 5 -> return "medium",  # ERROR: unreachable (subsumed by > 10)
        ? -> return "small"
    }
}

# Other unreachable pattern errors:
# - Wildcard before end: match (x) { ? -> "any", 5 -> "five" }  # ERROR
# - Duplicate patterns: match (x) { > 10 -> "a", > 10 -> "b" }  # ERROR
# - Overlapping ranges: match (x) { > 5 -> "a", >= 3 -> "b" }  # ERROR

Comparison Pattern Rules:

Match vs Select with Comparison Patterns:

Set Patterns ({…}) in Select Arms#

An arm's pattern may be a brace-delimited set of several discrete values, so callers don't repeat a result per value. The arm matches when the target equals any element; first-match-wins ordering is preserved.

classify(x<Int>)<String> -> {
    return select(x) -> {
        {1, 3, 5, 7, 9} -> "odd",
        {2, 4, 6, 8}    -> "even",
        ?               -> "out of bounds"
    }
}

enum Color { Red, Green, Blue }
name(c<Color>)<String> -> {
    return select(c) -> {
        {Red, Blue}  -> "primary",
        {Green}      -> "green",
        ?            -> "unknown"
    }
}

Set elements may be literals (Int/Float/String/Bool/null) or bare enum-variant names (North, Shape::Square). These are enforced at compile time:

Variadic Tuples#

Vyb supports fully variadic tuple types that can hold any number of heterogeneous elements (1 to N):

# Single-element tuples
get_single()<Tuple<Int>> -> {
    return 42  # Automatically wrapped in tuple struct
}

# Two-element tuples (dual syntax)
get_pair_inline()<Int, String> -> {
    return 10, "hello"  # Inline syntax
}

get_pair_generic()<Tuple<Int, String>> -> {
    return 20, "world"  # Generic syntax (equivalent)
}

# Multi-element tuples with mixed types
get_data()<Tuple<String, Int, Bool, Float>> -> {
    return "status", 200, true, 3.14
}

# Seven-element tuple demonstrating variadic support
get_complex()<Tuple<Int, Int, Bool, String, Int, Bool, Int>> -> {
    return 1, 2, true, "test", 3, false, 4
}

# Tuples work seamlessly with complex types
get_mixed()<Tuple<String, Vec<Int>, Bool>> -> {
    numbers<Vec<Int>> = Vec()
    numbers.push(1)
    numbers.push(2)
    return "data", numbers, true
}

main()<Int> -> {
    # All tuple syntaxes work identically
    single<Tuple<Int>> = get_single()
    pair<Tuple<Int,String>> = get_pair_inline()
    data<Tuple<String,Int,Bool,Float>> = get_data()

    return 0
}

Tuple Features:

String Theory#

Vyb's String type is a production-ready fat pointer implementation with comprehensive method support and natural literal syntax. Unlike C's null-terminated strings or C++'s heavyweight std::string, Vyb Strings combine the best of both worlds: efficient representation with modern conveniences.

String Structure#

# Internally, String is a fat pointer struct:
struct String {
    ptr: *i8,    # Pointer to null-terminated byte data
    len: i64     # Length (excluding null terminator)
}

This design provides:

Natural String Syntax#

String literals in Vyb are first-class citizens:

# Direct literal assignment
greeting<String> = "Hello, Vyb!"

# Literal concatenation (just works™)
message<String> = "Hello" + " " + "World"

# Method calls on literals
length<Int> = "Vyb".len()                    # Returns 3
first<Int> = "Quantum".char_at(0)            # Returns 'Q' (81)
check<Bool> = "Einstein".starts_with("Ein")  # Returns true

# All without explicit constructors!

String literals process backslash escapes: \n LF, \r CR, \t TAB, \\ a literal backslash, \" a double quote, \0 NUL, and \xHH a hex byte. Any other \X is kept verbatim (regex/path-friendly), so "\r\n" is a real CRLF pair — HTTP requests depend on this.

Complete Method Reference#

Constructor

# Create from raw bytes (C interop)
name<String> = String::from_bytes("Alice", 5)
raw<String> = String::from_bytes(c_ptr, c_len)

Property Access

msg<String> = "Hello"
length<Int> = msg.len()  # Returns 5, O(1) operation

Substring Operations

text<String> = "Hello World"

# Extract substring (end optional)
hello<String> = text.substring(0, 5)      # "Hello"
world<String> = text.substring(6, 11)     # "World"
rest<String> = text.substring(6)          # "World" (to end)

# Safe character access with bounds checking
first<Int> = text.char_at(0)      # 72 ('H')
space<Int> = text.char_at(5)      # 32 (' ')
invalid<Int> = text.char_at(99)   # 0 (null char for out of bounds)

Search and Comparison

sentence<String> = "The quick brown fox"

# Prefix/suffix checking
has_the<Bool> = sentence.starts_with("The")      # true
has_fox<Bool> = sentence.ends_with("fox")        # true
has_dog<Bool> = sentence.ends_with("dog")        # false

# Substring search
has_quick<Bool> = sentence.contains("quick")     # true
has_slow<Bool> = sentence.contains("slow")       # false

# Empty strings always match
always<Bool> = sentence.starts_with("")          # true

Case Conversion

mixed<String> = "Hello World"

# ASCII case conversion (allocates new string)
upper<String> = mixed.to_upper()   # "HELLO WORLD"
lower<String> = mixed.to_lower()   # "hello world"

# Original unchanged (immutability)
println(mixed)  # Still "Hello World"

String Concatenation

# Using + operator (most natural)
full<String> = "Hello" + " " + "World"

# Chaining operations
result<String> = "Code".to_upper() + " " + "Language".to_lower()
# Result: "CODE language"

# Mixed types auto-convert: "Count: " + 42
# message<String> = "Count: " + 42

Practical Examples#

Text Processing

process_input(text<String>)<Bool> -> {
    # Validate input
    if (text.len() == 0) {
        return false
    }

    # Check for command prefix
    if (text.starts_with("/")) {
        command<String> = text.substring(1)
        println("Command: " + command)
        return true
    }

    # Search for keywords
    if (text.contains("help")) {
        println("Help requested")
        return true
    }

    return false
}

String Manipulation

format_name(first<String>, last<String>)<String> -> {
    # Capitalize first letter of each name
    first_upper<String> = first.char_at(0).to_string().to_upper() +
                          first.substring(1).to_lower()

    last_upper<String> = last.char_at(0).to_string().to_upper() +
                         last.substring(1).to_lower()

    # Combine with space
    return first_upper + " " + last_upper
}

main()<Int> -> {
    formatted<String> = format_name("ALICE", "wonderland")
    println(formatted)  # "Alice Wonderland"
    return 0
}

Data Validation

validate_email(email<String>)<Bool> -> {
    # Simple email validation
    if (email.len() < 3) {
        return false  # Too short
    }

    if (!email.contains("@")) {
        return false  # No @ symbol
    }

    return true
}

Memory Management#

Allocation Strategy

# Read-only operations: ZERO allocations
len<Int> = "Hello".len()                    # No malloc
ch<Int> = "World".char_at(0)                # No malloc
check<Bool> = "Test".starts_with("Te")      # No malloc

# Transform operations: Allocate new strings
upper<String> = "hello".to_upper()          # malloc(6) for "HELLO\0"
sub<String> = "Hello World".substring(0, 5) # malloc(6) for "Hello\0"
concat<String> = "A" + "B"                  # malloc(3) for "AB\0"

# Reference-counted our<T> handles cleanup automatically when the last strong
# reference leaves scope with no strong references remaining

Bounds Safety

# All index operations are bounds-checked at runtime
text<String> = "Vyb"

safe<Int> = text.char_at(2)      # OK: returns 'n' (110)
safe2<Int> = text.char_at(0)     # OK: returns 'V' (86)

# Out of bounds returns safe defaults
oob1<Int> = text.char_at(-1)     # Returns 0 (null char)
oob2<Int> = text.char_at(100)    # Returns 0 (null char)

# substring returns empty string for invalid bounds
empty<String> = text.substring(10, 20)  # Returns {null, 0}

String Performance Characteristics#

Operation Time Space Notes
len() O(1) O(1) Just reads struct field
char_at(i) O(1) O(1) Bounds-checked array access
starts_with(s) O(k) O(1) k = prefix length, uses memcmp
ends_with(s) O(k) O(1) k = suffix length, uses memcmp
contains(s) O(n×k) O(1) Uses C strstr (KMP-like)
substring(i,j) O(k) O(k) k = j-i, allocates new buffer
to_upper() O(n) O(n) Allocates new buffer, ASCII only
to_lower() O(n) O(n) Allocates new buffer, ASCII only
split(sep) O(n×k) O(n) k = separator length; allocates a new buffer per part
format(args...) O(n+a) O(n+a) n = fmt length, a = serialized argument bytes
a + b O(n+m) O(n+m) Allocates new buffer

C Interoperability#

All String methods produce null-terminated strings for C compatibility:

# Use with C functions via the FFI (extern "C" + typed C aliases)
name<String> = "Alice"
c_str<*i8> = name.to_bytes()  # Get raw pointer

# Interoperable with:
# printf("%s", c_str)
# strlen(c_str)
# strcmp(c_str1, c_str2)
# strstr(haystack, needle)

Concatenation & Chaining#

# Simple things are simple
msg<String> = "Hello" + " " + "World"

# Complex things are possible (when needed)
advanced<String> = data
    .to_lower()
    .substring(0, 100)
    .replace("old", "new")
    .trim()

String Theory Achievement Unlocked: You now understand Vyb Strings better than most physicists understand actual string theory! 🎻✨

Error Handling with trap/fail/ensure#

Vyb features an explicit error handling system that combines the clarity of exceptions with the safety of Result types. The trap/fail/ensure trio provides compile-time error tracking with zero runtime overhead for the happy path.

The Philosophy#

Traditional error handling offers two flawed extremes:

Vyb's approach:

Error Fundamentals#

Functions declare error returns in their signatures using failable types:

# Functions that can fail return (T, error_ptr) tuples
divide(a<Int>, b<Int>)<Int> -> {
    if (b == 0) {
        fail 42  # Return error with code 42
    }
    return a / b
}

# The compiler wraps this as: { i64 result, i8* error } in LLVM

Type System Integration:

The fail Keyword#

Create and propagate errors with fail:

# Simple error codes (primitive types)
fail 404                    # Integer error
fail "not found"            # String error
fail 3.14                   # Float error

# Structured errors (custom types)
struct DivisionError {
    code<Int>,
    dividend<Int>,
    divisor<Int>
}

divide_structured(a<Int>, b<Int>)<Int> -> {
    if (b == 0) {
        fail DivisionError {
            code = 42,
            dividend = a,
            divisor = b
        }
    }
    return a / b
}

Error Memory Layout:

Heap-allocated error struct (16 bytes):
  Offset 0-7:  Type ID hash (i64) - For pattern matching
  Offset 8-15: Error value/data    - Primitive or struct data

Type ID Hashing:

The trap Block#

Handle errors with pattern matching:

# Basic trap with single error type
compute_safely(x<Int>, y<Int>)<Int> -> {
    result<Int> = {
        value<Int> = divide(x, y)  # May fail
        value * 2
    } trap (e<Int>) -> {
        println("Division failed with code: " + String::from_int(e))
        -1  # Return fallback value
    }
    return result
}

# Multiple error types with pattern matching
process_data(x<Int>)<String> -> {
    {
        value<Int> = risky_operation(x)
        "Success: " + String::from_int(value)
    } trap (e<Int>) -> {
        "Integer error: " + String::from_int(e)
    } trap (e<String>) -> {
        "String error: " + e
    } trap (e<DivisionError>) -> {
        "Division error code: " + String::from_int(e.code)
    }
}

# Wildcard trap - catch any error type
safe_operation(x<Int>)<Int> -> {
    result<Int> = {
        risky_call(x)
    } trap (e<DivisionError>) -> {
        println("Known error: division by zero")
        return 0
    } trap (e<?>) -> {
        println("Unknown error caught by wildcard!")
        return -1
    }
    return result
}

# Multi-type trap - catch union of error types
process_data(input<String>)<Int> -> {
    result<Int> = {
        parse_and_validate(input)
    } trap (e<ParseError | ValidationError>) -> {
        # Handler matches either ParseError or ValidationError
        println("Input processing failed")
        return -1
    } trap (e<NetworkError | TimeoutError>) -> {
        # Different handler for network-related errors
        println("Network issue, retrying...")
        return retry_operation(input)
    }
    return result
}

Pattern Matching Features:

Error Propagation#

Errors automatically propagate through call stacks:

# Three-level error propagation
divide(a<Int>, b<Int>)<Int> -> {
    if (b == 0) {
        fail 42  # Create error at bottom
    }
    return a / b
}

compute(x<Int>, y<Int>)<Int> -> {
    result<Int> = divide(x, y)  # Propagates error if divide fails
    return result + 10           # Only reached if divide succeeds
}

main()<Int> -> {
    val1<Int> = compute(10, 2)   # OK: 10/2 + 10 = 15

    val2<Int> = {
        compute(10, 0)  # Fails: division by zero
    } trap (e<Int>) -> {
        println("Caught error: " + String::from_int(e))
        -1  # Return fallback
    }

    return val1 + val2  # Returns 15 + (-1) = 14
}

Propagation Mechanics:

  1. divide(10, 0) allocates error on heap, returns (undef, error_ptr)
  2. compute checks error pointer, finds non-null, propagates (undef, error_ptr)
  3. main trap block catches error, extracts type and value
  4. Error memory freed after handling

Untrapped Errors#

Errors that escape without trap handlers trigger runtime termination:

divide(a<Int>, b<Int>)<Int> -> {
    if (b == 0) {
        fail 42
    }
    return a / b
}

main()<Int> -> {
    result<Int> = divide(10, 0)  # No trap handler!
    return result
}

Runtime Output:

┌─ UNTRAPPED FAILURE ──────────────────────────────────────────┐
│ Error: <runtime error>                                       │
│ Thread: 6472648897627130861                                  │
│ Time: 2025-10-21 10:01:29.810                                │
└──────────────────────────────────────────────────────────────┘

Exit Code: 1

Safety Guarantees:

Struct Errors with Context#

Rich error types carry debugging context:

struct ValidationError {
    field_name<String>,
    expected<String>,
    actual<String>,
    line_number<Int>
}

validate_input(input<String>)<Bool> -> {
    if (input.len() == 0) {
        fail ValidationError {
            field_name = "input",
            expected = "non-empty string",
            actual = "empty string",
            line_number = 42
        }
    }
    return true
}

main()<Int> -> {
    {
        is_valid<Bool> = validate_input("")
        0
    } trap (e<ValidationError>) -> {
        println("Validation failed:")
        println("  Field: " + e.field_name)
        println("  Expected: " + e.expected)
        println("  Actual: " + e.actual)
        println("  Line: " + String::from_int(e.line_number))
        1
    }
}

Output:

Validation failed:
  Field: input
  Expected: non-empty string
  Actual: empty string
  Line: 42

The ensure Keyword ✅ IMPLEMENTED#

The ensure keyword provides cleanup/finally semantics, guaranteeing code runs whether the block succeeds or fails:

# The ensure block runs on both success and failure paths
process_file(path<String>)<String> -> {
    file<File> = open_file(path)

    result<String> = {
        file.read()
    } trap (e<IOError>) -> {
        return ""
    } ensure -> {
        file.close()  # Always runs, success or failure
    }

    return result
}

# Execution order on error:
# 1. Block code executes and fails
# 2. Matching trap handler runs (if present)
# 3. ensure block always runs last

# Implementation details:
# - Parser fully supports } ensure -> { } syntax
# - Codegen inlines ensure blocks into control flow
# - Works with both success and failure paths
# - See: test/trap/test_ensure_simple.vyb

LLVM Code Generation#

The error handling system compiles to efficient LLVM IR:

; Failable function returning {i64 result, i8* error}
define { i64, ptr } @divide(i64 %a, i64 %b) {
entry:
  %is_zero = icmp eq i64 %b, 0
  br i1 %is_zero, label %error_path, label %success_path

error_path:
  ; Allocate error struct: 8 bytes type_id + 8 bytes value
  %error_mem = call ptr @malloc(i64 16)

  ; Store type ID at offset 0
  %type_id = i64 -3994496327427856726  ; hash("Int")
  store i64 %type_id, ptr %error_mem

  ; Store error value at offset 8
  %value_ptr = getelementptr i8, ptr %error_mem, i64 8
  store i64 42, ptr %value_ptr

  ; Return (undef, error_ptr) tuple
  %result = insertvalue { i64, ptr } undef, ptr %error_mem, 1
  ret { i64, ptr } %result

success_path:
  %quotient = sdiv i64 %a, %b
  ; Return (quotient, null) tuple
  %success = insertvalue { i64, ptr } undef, i64 %quotient, 0
  %success2 = insertvalue { i64, ptr } %success, ptr null, 1
  ret { i64, ptr } %success2
}

; Call site with error checking
define { i64, ptr } @compute(i64 %x, i64 %y) {
entry:
  %call = call { i64, ptr } @divide(i64 %x, i64 %y)
  %value = extractvalue { i64, ptr } %call, 0
  %error = extractvalue { i64, ptr } %call, 1

  ; Check if error occurred
  %has_error = icmp ne ptr %error, null
  br i1 %has_error, label %error_propagate, label %success

error_propagate:
  ; Propagate error up the stack
  %prop = insertvalue { i64, ptr } undef, ptr %error, 1
  ret { i64, ptr } %prop

success:
  %result = add i64 %value, 10
  %ret = insertvalue { i64, ptr } undef, i64 %result, 0
  %ret2 = insertvalue { i64, ptr } %ret, ptr null, 1
  ret { i64, ptr } %ret2
}

; Trap handler with type matching
define i64 @main() {
entry:
  %trap_error = alloca ptr
  store ptr null, ptr %trap_error
  br label %try_block

try_block:
  %call = call { i64, ptr } @compute(i64 10, i64 0)
  %value = extractvalue { i64, ptr } %call, 0
  %error = extractvalue { i64, ptr } %call, 1
  %has_error = icmp ne ptr %error, null
  br i1 %has_error, label %trap_landing, label %try_success

trap_landing:
  ; Load error and check type
  %error_typeid = load i64, ptr %error
  %type_matches = icmp eq i64 %error_typeid, -3994496327427856726
  br i1 %type_matches, label %catch_handler, label %unmatched

catch_handler:
  ; Extract error value from offset 8
  %value_ptr = getelementptr i8, ptr %error, i64 8
  %error_value = load i64, ptr %value_ptr

  ; Free error memory
  call void @free(ptr %error)

  ; Handle error (return -1)
  br label %after_trap

unmatched:
  ; No matching handler - call runtime
  call void @__vyb_runtime_untrapped_error(ptr %error)
  unreachable

try_success:
  br label %after_trap

after_trap:
  %result = phi i64 [ %value, %try_success ], [ -1, %catch_handler ]
  ret i64 %result
}

Error-Handling Performance Characteristics#

Happy Path (No Errors):

Error Path:

Error Handling Best Practices#

1. Use Specific Error Types

# Good: Rich context
struct FileError {
    path<String>,
    operation<String>,
    reason<String>
}

# Less good: Generic integer codes
fail 404

2. Handle Errors Close to Source

# Good: Handle immediately if recovery is possible
result<String> = {
    read_file(path)
} trap (e<FileError>) -> {
    println("Using default config due to: " + e.reason)
    default_config()
}

# Sometimes okay: Let errors propagate if caller should decide
data<String> = read_file(path)  # Propagates error to caller

3. Document Error Conditions

# Read configuration from file
#
# Errors:
#   FileError { operation = "open", ... } - File doesn't exist
#   FileError { operation = "read", ... } - Permission denied
#   ParseError { ... } - Invalid TOML format
read_config(path<String>)<Config> -> {
    # Implementation...
}

4. Prefer Structured Errors Over Codes

# Good: Self-documenting
struct NetworkError {
    url<String>,
    status_code<Int>,
    retry_after<Int>
}

# Less good: Requires external documentation
fail 503  # What does this mean?

Example: Complete Error Handling#

A comprehensive example showing all error handling features:

# Define domain-specific error types
struct ParseError {
    input<String>,
    position<Int>,
    expected<String>
}

struct ValidationError {
    field<String>,
    constraint<String>
}

struct DatabaseError {
    query<String>,
    code<Int>
}

# Parse integer from string
parse_int(s<String>)<Int> -> {
    # Actual parsing logic would go here
    if (s.len() == 0) {
        fail ParseError {
            input = s,
            position = 0,
            expected = "non-empty string"
        }
    }
    return 42  # Simplified
}

# Validate age constraint
validate_age(age<Int>)<Bool> -> {
    if (age < 0) {
        fail ValidationError {
            field = "age",
            constraint = "must be non-negative"
        }
    }
    if (age > 150) {
        fail ValidationError {
            field = "age",
            constraint = "must be <= 150"
        }
    }
    return true
}

# Store user in database (can fail)
store_user(name<String>, age<Int>)<Int> -> {
    valid<Bool> = validate_age(age)  # Propagates ValidationError

    # Simulate database operation
    if (age == 42) {
        fail DatabaseError {
            query = "INSERT INTO users",
            code = 1062  # Duplicate entry
        }
    }

    return 1  # User ID
}

# Process user registration with comprehensive error handling
register_user(name<String>, age_str<String>)<String> -> {
    # Multi-level error handling with different error types
    result<String> = {
        # Parse age (may fail with ParseError)
        age<Int> = parse_int(age_str)

        # Store user (may fail with ValidationError or DatabaseError)
        user_id<Int> = store_user(name, age)

        "User registered with ID: " + String::from_int(user_id)

    } trap (e<ParseError>) -> {
        msg<String> = "Parse failed: " + e.expected +
                     " at position " + String::from_int(e.position)
        println(msg)
        "PARSE_ERROR"

    } trap (e<ValidationError>) -> {
        msg<String> = "Validation failed for " + e.field +
                     ": " + e.constraint
        println(msg)
        "VALIDATION_ERROR"

    } trap (e<DatabaseError>) -> {
        msg<String> = "Database error " + String::from_int(e.code) +
                     " in query: " + e.query
        println(msg)
        "DATABASE_ERROR"
    }

    return result
}

main()<Int> -> {
    # Test success path
    result1<String> = register_user("Alice", "25")
    println(result1)  # "User registered with ID: 1"

    # Test ParseError
    result2<String> = register_user("Bob", "")
    println(result2)  # "PARSE_ERROR"

    # Test ValidationError
    result3<String> = register_user("Charlie", "-5")
    println(result3)  # "VALIDATION_ERROR"

    # Test DatabaseError
    result4<String> = register_user("Dave", "42")
    println(result4)  # "DATABASE_ERROR"

    return 0
}

Key Takeaways:

The Vyb error handling system achieves the rare combination of safety, performance, and ergonomics that makes robust error handling a joy rather than a chore.

Build System#

Vyb uses CMake for building:

# Clean build
mkdir -p build && cd build
cmake .. && make clean && make -j

# Quick rebuild (from project root)
make -C build -j

# Run a single file
build/vyb test/string/string_test.vyb

# Run demos and examples
build/vyb test/run_examples.vyb --vyb build/vyb
cmake --build build --target run-examples

# Run the milestone gate
build/vyb test/run_milestone.vyb --vyb build/vyb
cmake --build build --target run-milestone

Test Harness#

Vyb's canonical test runner is test/run_tests.vyb — a Vyb program, the same suite wired into CTest as the run-tests target and used for the full regression gate (currently 1314 .vyb tests, all passing):

Quick Testing#

# Full suite (JIT execution), from the repo root
build/vyb test/run_tests.vyb --vyb build/vyb --test-dir test

# Run a single category (parse, semantic, async, tls, ...)
build/vyb test/run_tests.vyb --vyb build/vyb --test-dir test --category async

# Split the suite across cores: K is 0-based, N is the shard count
build/vyb test/run_tests.vyb --vyb build/vyb --test-dir test --shard 0/4 &

# Save results as JSON, or full run evidence
build/vyb test/run_tests.vyb --vyb build/vyb --test-dir test --json results.json
build/vyb test/run_tests.vyb --vyb build/vyb --test-dir test --evidence evidence.json

The runner is single-threaded, so --shard K/N is how CI (.github/workflows/ci.yml) runs the suite on several cores at once: it walks the same deterministic discovery list and keeps every Nth entry, so the shards are disjoint and complete. --list prints what an invocation selects without running it, and CI proves the partition by diffing the union of the four shard lists against the full list. The compiler build is cached through ccache, so a re-run does not rebuild it from scratch.

vyb --repo (or vyb test with no explicit paths inside the compiler repo) runs the same Vyb runner, so the compiler's own test path is written in Vyb end to end. A secondary harness (test_harness.vyb) plus triage_tool.vyb — both Vyb programs — add JSON/HTML reporting, failure-triage, and performance analysis on top of that suite; the authoritative pass/fail count is always the run-tests CTest target output.

Test Analysis and Triage#

# Analyze test failures and create triage plan
build/vyb triage_tool.vyb results.json

# Generate markdown triage report
build/vyb triage_tool.vyb results.json --output-format markdown --output triage.md

# Focus on critical issues only
build/vyb triage_tool.vyb results.json --priority critical,high

Test Features#

GPU Kernels (CUDA / NVPTX)#

Vyb lowers a module to pure NVPTX device code with --kernel, emits PTX, and validates it with ptxas in the same step (a rejection is a hard error). Kernel mode has no host runtime: the string registry, heap allocator and call-stack runtime do not exist on the device, so kernels use the device intrinsics — tid_x, blk_x, dim_x, grid_x, ld_f32/ld_f64/ld_i32/ld_i64, st_f32/st_f64/st_i32, atomic_add_i32/atomic_add_f64, kernel_barrier, ld_shared_f64.

Emit and validate#

# a value/data-parallel kernel with no host references
build/vyb --kernel fixtures/kernel/axpy.vyb --ptx axpy.ptx            # default sm_86
build/vyb --kernel fixtures/kernel/axpy.vyb --ptx axpy.ptx --gpu sm_90
# -> PTX validated by ptxas (sm_86): axpy.ptx.cubin
// y = alpha * x + y — fixtures/kernel/axpy.vyb
axpy(alpha<Float>, x<Float>, y<Float>)<Float> -> {
    return alpha * x + y
}

Run a kernel on the GPU#

The signed cuda binding wraps the driver API. A kernel takes one argument — a device pointer to an array of i64 descriptor words, read in-kernel with ld_i64. The smallest complete example fills a device buffer:

build/vyb fixtures/cuda/launch_fill.vyb --module-path bindings --module-path bindings/cuda
# -> 42

Bigger worked examples: fixtures/kernel/matmul_smem.vyb (shared-memory tiled matmul) with fixtures/cuda/matmul_verify.vyb, and vendor cross-validation against cuBLAS, cuFFT and cuDNN in fixtures/cuda/.

Rules that save a bad afternoon#

Full reference: doc/CUDA.md.

Project Structure#

Vyb/
├── src/              # C++ source code
│   ├── main.cpp      # Entry point with LLVM JIT
│   ├── lexer.cpp     # Tokenization
│   ├── parser.cpp    # Syntax analysis
│   └── ast.cpp       # Abstract syntax tree
├── include/vyb/      # Header files
├── test/             # Vyb test programs
├── examples/         # Example programs
├── doc/              # Documentation
└── build/            # Build output

Examples#

Simple Programs#

Hello World:

main() -> {
    println("Hello, Vyb!")
}

Mathematical Computation:

fibonacci(n<Int>)<Int> -> {
    if (n <= 1) {
        return n
    } else {
        return fibonacci(n - 1) + fibonacci(n - 2)
    }
}

main()<Int> -> {
    return fibonacci(10)  # Returns 55
}

Data Processing with Auto-Serialization:

struct Result {
    success<Bool>,
    value<Int>,
    message<String>
}

process_data(x<Int>)<Result> -> {
    if (x > 0) {
        return Result {
            success = true,
            value = x * 2,
            message = "Processing successful"
        }
    } else {
        return Result {
            success = false,
            value = 0,
            message = "Invalid input"
        }
    }
}

main()<Result> -> {
    return process_data(21)
}
# Outputs structured JSON for the Result

JSON Serialization & Deserialization#

Vyb includes a complete JSON serialization system with bidirectional conversion between structs and JSON:

Automatic Serialization#

struct Person {
    name<String>,
    age<Int>,
    active<Bool>
}

main()<Int> -> {
    person<Person> = Person { name: "Alice", age: 30, active: true }

    # Serialize to JSON string
    json<String> = person.to_string()
    println(json)  # Output: {"name": "Alice", "age": 30, "active": true}

    return 0
}

JSON Deserialization#

# Deserialize JSON back to struct
person2<Person> = Person::from_string(json)

# Access fields normally
println(person2.name)  # Output: Alice
println(person2.age.to_string())  # Output: 30

Supported Types#

Auto-Serialization for main() Returns#

One of Vyb's standout features is automatic serialization of complex return types from main():

Implementation Details#

The JSON system is built on:

See test/json/ for comprehensive examples and runtime/vyb_type_metadata.c for implementation.

This makes Vyb excellent for data processing scripts, API services, and configuration management.

Memory Safety#

Vyb provides multiple memory management strategies:

# Unique ownership: one owner, RAII cleanup
owned<my<String>> = my("unique data")

# Shared ownership (reference counted)
shared<our<String>> = our("shared data")
another_ref<our<String>> = shared  # Reference count incremented

# Borrowing (non-owning references)
view_ref<their<String>> = view(shared)      # Immutable borrow
mut_ref<their<String>> = borrow(owned)      # Mutable borrow

# Raw pointers for freedom operations
freedom {
    x<Int> = 42
    ptr<loc<Int>> = loc(x)  # Get pointer to x
    at(ptr) = 99           # Modify through pointer
}

Testing & Development Tools#

Vyb includes a modern, comprehensive testing infrastructure designed for efficient development and quality assurance:

🧪 Modern Test Harness#

The Vyb test harness (test_harness.vyb) layers reporting and triage on top of the canonical runner:

# Basic harness run over a directory
build/vyb test_harness.vyb --test-dirs test/units

# JSON + HTML reporting
build/vyb test_harness.vyb --test-dirs test/units --json-report results.json --html-report report.html

# Filter by pattern, category, tags, or priority
build/vyb test_harness.vyb --test-dirs test/units --pattern "test_async_*.vyb"
build/vyb test_harness.vyb --test-dirs test/units --category parser,semantic --priority high -v

The harness runs the selected files sequentially (--workers is accepted for compatibility) and reports the same per-test verdicts as the canonical runner.

Test Harness Features#

Test Statistics#

🔧 Syntax Migration Tools#

Automated tools ensure codebase consistency and syntax standardization:

# Scan for legacy syntax patterns
build/vyb migrate_syntax.vyb --scan --directory . --report

# Apply canonical syntax migrations with backup
build/vyb migrate_syntax.vyb --migrate --directory . --backup

Migration Capabilities#

📊 Triage Analysis Tool#

Automated failure analysis and development prioritization:

# Generate a triage report from test results
build/vyb triage_tool.vyb test_results.json --output-format markdown --output triage_report.md

Triage Features#

📈 Development Workflow#

The integrated toolchain supports efficient development:

  1. Write Code: Use canonical syntax with ownership types
  2. Run Tests: build/vyb test_harness.vyb --test-dirs test/units --json-report results.json
  3. Check Syntax: build/vyb migrate_syntax.vyb --scan
  4. Debug Issues: Use triage reports for prioritized debugging
  5. Commit Changes: Regular Git commits with test validation

Architecture#

Vyb is built on solid foundations:

Contributing#

Vyb is actively developed with regular commits tracking progress:

  1. Language Features: Add new syntax, types, or operations
  2. Standard Library: Implement core modules and utilities
  3. Testing: Create comprehensive test cases
  4. Documentation: Improve guides and examples
  5. Performance: Optimize compilation and runtime

See doc/ directory for detailed design documents and RFCs.

Recent Progress#

Latest cycle (v0.7.x): stdlib concurrency + a network/UI demo

v0.4.2 (freedom-1.0 series): Generic function monomorphization and FREEDOM blocks

Language Status: Vyb (freedom-1.0 series, tracked as v0.7.x) is an actively developed systems programming language with unified canonical syntax, a sized type system (Int8–Int64, UInt8–UInt64, Float32/Float64, Char, Rune, Bytes), compile-time monomorphized generics, aspect/bind polymorphism, a fail/trap error system, JIT/AOT/native codegen, and a modern test harness. The core language is stable and well tested; see doc/FEATURE_STATUS.md for the current feature matrix.

Getting Help#


Appendix#

A. Grammar (EBNF)#

The formal EBNF grammar now lives in the canonical reference, docs/refman/PROGRAMMERS_GUIDE.md §Appendix D — Grammar (EBNF). It is consolidated there so the project has one grammar home; the appendix also notes which productions are legacy.

B. Memory Model Reference#

Ownership Types:

Borrowing Operations:

Freedom Operations:

C. Auto-Serialization Reference#

Vyb automatically serializes complex return types from main():

Simple Returns:

Complex Returns:

Customization: Implement Serialize aspect for custom serialization behavior.

Glossary#

AST (Abstract Syntax Tree) Tree representation of source code structure produced by the parser.

Borrow Checking Compile-time analysis ensuring references don't outlive the data they point to.

Bundle Module visibility grouping via bundle(...) / share(...) directives.

Import Secure module inclusion from verified, signed sources.

JIT (Just-In-Time) Runtime compilation of code to native machine instructions for performance.

LLVM Low Level Virtual Machine - compiler infrastructure used by Vyb's backend.

Monomorphization Compile-time process of creating specialized versions of generic functions/types.

Ownership Memory management system using my<T>, our<T>, their<T>, mild<T> types.

Pattern Matching match statements that destructure and test values against patterns.

Smuggle Flexible module inclusion from external, potentially unverified sources.

Template Compile-time generic construct parameterized by types or constants.

Vec Resizable array collection with methods like push(), pop(), len(), get().


License#

Copyright 2026 Aniviza LLC. Licensed under the Apache License, Version 2.0 — see the LICENSE file for the full text.


Vyb (freedom-1.0 series): A mature systems programming language with a comprehensive sized type system, unified name-first syntax, pattern matching, compile-time monomorphized generics, resizable collections, an aspect/bind polymorphism system, and a unique import/smuggle module system.

↑