Skip to content

The Wyn Tour

Welcome. This is the fast, hands-on path from "never seen Wyn" to "shipping a concurrent program." If you write Python, you already read Wyn — it has the same low-ceremony feel: bare assignment, string interpolation, for k, v in map. The difference is what happens next: Wyn compiles straight to C, links no VM and no garbage collector, and drops a ~50KB native binary that starts instantly.

Everything below is a real program. Paste any block into a .wyn file and wyn run it — every snippet on this page was compiled and run against the current compiler (v1.17). Let's go.


0. Install & Hello, World

Install Wyn (one line — see Installation for every platform):

sh
curl -fsSL https://wynlang.com/install.sh | sh

Confirm your toolchain is healthy:

sh
wyn doctor

wyn doctor showing all green checkmarks and a healthy toolchain

  ✓ TCC runtime
  ✓ System C compiler (cc)
  ✓ Precompiled runtime (system cc)
  ✓ git (for `wyn add` dependencies)

✓ All good! Using fast compile path (system cc + precompiled runtime).

Now the traditional greeting. Put this in hello.wyn:

wyn
fn main() {
    println("Hello, Wyn!")
}
sh
wyn run hello.wyn

wyn run hello.wyn printing Hello, Wyn!

Hello, Wyn!

wyn run compiles with a fast in-memory backend — great for iteration. When you're ready to ship, wyn build hello.wyn produces a standalone native binary. More on that at the end.


1. Variables & types

Assignment is bare — no keyword needed, types are inferred. Reach for var when you want to be explicit that something is mutable, and const for compile-time constants. Strings interpolate with ${...}.

wyn
fn main() {
    var count = 10          // mutable, type inferred as int
    const MAX = 100         // constant
    count = count + 1

    var name = "Wyn"        // string
    var ratio = 3.14        // float
    var ok = true           // bool

    println("count=${count}, max=${MAX}")
    println("Language: ${name}, len ${name.len()}, up ${name.upper()}")
    println("ratio=${ratio}, ok=${ok}")
}

wyn run output of the variables snippet

count=11, max=100
Language: Wyn, len 3, up WYN
ratio=3.14, ok=true

The core scalar types are int (64-bit), float (double), bool, and string. Wyn is statically typed, but inference means you rarely write a type unless you want the documentation.


2. Functions

The full form is fn name(params) -> ReturnType { ... }. When a function is just one expression, use the arrow body -> T => expr. Parameters can have defaults.

wyn
// Block body.
fn add(a: int, b: int) -> int { return a + b }

// Expression body — no braces, no `return`.
fn square(n: int) -> int => n * n

// Default argument.
fn greet(who: string = "world") -> string { return "hello, ${who}" }

fn main() {
    println(add(2, 3).to_string())        // 5
    println(square(6).to_string())        // 36
    println(greet())                      // hello, world
    println(greet("Wyn devs"))            // hello, Wyn devs
}

wyn run output of the functions snippet

5
36
hello, world
hello, Wyn devs

3. Control flow

if/else, for … in over ranges, and while. Ranges come in exclusive (0..n) and inclusive (1..=n) flavors, and range(a, b, step) gives you a stride. Logical operators are wordsand, or, not — not symbols.

wyn
fn main() {
    var count = 11

    if count > 10 {
        println("big")
    } else {
        println("small")
    }

    for i in 0..3 { println("i=${i}") }        // 0, 1, 2

    var total = 0
    for i in 1..=5 { total = total + i }        // inclusive: 1+2+3+4+5
    println("sum=${total}")                     // 15

    // while, with break and continue
    var i = 0
    while true {
        if i >= 5 { break }
        if i == 2 { i = i + 1; continue }
        print(i)
        i = i + 1
    }
    println("")

    // Words, not symbols.
    var flag = true and not false
    var either = false or true
    println("flag=${flag}, either=${either}")

    // strided range
    for n in range(0, 10, 2) { print(n) }       // 0 2 4 6 8
    println("")
}

wyn run output of the control-flow snippet

big
i=0
i=1
i=2
sum=15
34
flag=true, either=true
02468

4. Collections

Arrays, maps, and sets — with slicing, negative indexing, and the ergonomic for k, v in map. zip() walks two collections in lockstep.

wyn
fn main() {
    var xs = [1, 2, 3, 4, 5]
    print(xs[0])       // 1
    print(xs[-1])      // 5  (negative index counts from the end)
    print(xs[1:3])     // [2, 3]  (slice)
    xs.push(6)
    println("len=${xs.len()}")

    // HashMap literal
    var m = {"a": 1, "b": 2}
    m["c"] = 3
    for k, v in m { println("${k} -> ${v}") }

    // HashSet of strings — {: ... }
    var seen = {: "red", "green", "blue" }
    println("has red? ${seen.contains("red")}")

    // zip two arrays into pairs
    var names = ["ada", "bob"]
    var ages  = [36, 42]
    for who, age in zip(names, ages) { println("${who} is ${age}") }
}

wyn run output of the collections snippet

1
5
[2, 3]
len=6
a -> 1
c -> 3
b -> 2
has red? true
ada is 36
bob is 42

TIP

Arrays and maps print with print(xs), which formats them for you ([2, 3]). Interpolating a whole collection into a string with "${xs}" isn't supported — print the collection directly, or interpolate its elements.


5. Structs & methods

Structs group data; methods live in the body (or an impl block) and take self. Construct with Name { field: value }.

wyn
struct Point {
    x: int,
    y: int
    fn dist2(self) -> int { return self.x * self.x + self.y * self.y }
    fn shifted(self, dx: int, dy: int) -> Point {
        return Point { x: self.x + dx, y: self.y + dy }
    }
}

fn main() {
    var p = Point { x: 3, y: 4 }
    println("dist2=${p.dist2()}")       // 25

    var q = p.shifted(1, 1)
    println("${q.x},${q.y}")            // 4,5
}

wyn run output of the structs snippet

dist2=25
4,5

6. Enums & pattern matching

Enums can carry payloads, and match destructures them. Best of all, match is an expression — it produces a value you can return or assign.

wyn
enum Shape {
    Circle(int),
    Rect(int, int),
}

fn area(s: Shape) -> int {
    return match s {
        Circle(r)  => r * r * 3,
        Rect(w, h) => w * h,
    }
}

fn main() {
    println("circle area=${area(Shape.Circle(2))}")   // 12
    println("rect area=${area(Shape.Rect(3, 5))}")     // 15
}

wyn run output of the enums and match snippet

circle area=12
rect area=15

7. Error handling — Result, Option, and ?

Wyn has no exceptions and no null. Fallible functions return Result<T, E>; maybe-absent values are Option<T> (spelled T?). The ? operator propagates an error early, so the happy path stays flat.

wyn
fn safe_div(a: int, b: int) -> Result<int, string> {
    if b == 0 { return Err("division by zero") }
    return Ok(a / b)
}

fn parse_port(s: string) -> Result<int, string> {
    if s == "" { return Err("empty") }
    return Ok(8080)
}

// `?` returns early on Err — no nesting.
fn checked(s: string) -> Result<int, string> {
    var p = parse_port(s)?
    return Ok(p + 1)
}

fn find(arr: [int], target: int) -> Option<int> {
    for i in 0..arr.len() {
        if arr[i] == target { return Some(i) }
    }
    return None
}

fn main() {
    match safe_div(10, 2) {
        Ok(v)  => println("ok: ${v}"),
        Err(e) => println("err: ${e}"),
    }
    match safe_div(10, 0) {
        Ok(v)  => println("ok: ${v}"),
        Err(e) => println("err: ${e}"),
    }

    // unwrap_or supplies a default
    println("${checked("x").unwrap_or(-1)}")

    match find([10, 20, 30], 20) {
        Some(idx) => println("found at ${idx}"),
        None      => println("not found"),
    }

    // `if let` binds only the Some case
    if let Some(idx) = find([1, 2, 3], 3) {
        println("if-let got ${idx}")
    }
}

wyn run output of the error-handling snippet

ok: 5
err: division by zero
8081
found at 1
if-let got 2

Optional chaining with ?. reaches through a maybe-absent value; the whole expression short-circuits to None if any link is missing.

wyn
struct User { name: string, age: int }

fn find_user(id: int) -> User? {
    if id == 1 { return Some(User { name: "Ada", age: 36 }) }
    return None
}

fn main() {
    var name = find_user(1)?.name
    println("${name.unwrap_or("?")}")          // Ada

    var missing = find_user(2)?.name
    println("${missing.unwrap_or("nobody")}")  // nobody
}
Ada
nobody

8. Generics & traits

Generic functions abstract over a type parameter <T>. Traits describe shared behavior, and structs implement them with impl Trait for Struct.

wyn
fn identity<T>(x: T) -> T { return x }

trait Drawable {
    fn draw(self) -> string
}

struct Circle { r: int }
struct Rect { w: int, h: int }

impl Drawable for Circle {
    fn draw(self) -> string { return "circle(${self.r})" }
}
impl Drawable for Rect {
    fn draw(self) -> string { return "rect(${self.w}x${self.h})" }
}

// Accept anything that implements the trait (dynamic dispatch).
fn render(shape: Drawable) { println(shape.draw()) }

fn main() {
    println("${identity(99)}")          // works on int
    println("${identity("generic")}")   // …and string

    var c = Circle { r: 5 }
    var r = Rect { w: 10, h: 20 }
    render(c)                            // circle(5)
    render(r)                            // rect(10x20)
}

wyn run output of the generics and traits snippet

99
generic
circle(5)
rect(10x20)

9. Closures & higher-order functions

Lambdas use the arrow form (params) => expr and capture their environment. Pass them to .map, .filter, and .reduce, or to your own functions typed fn(int) -> int.

wyn
fn apply_twice(f: fn(int) -> int, x: int) -> int { return f(f(x)) }

fn main() {
    // Capturing closure
    var base = 100
    var addbase = (n) => n + base
    println("${addbase(5)}")               // 105

    // Passing a lambda as an argument
    var double = (n) => n * 2
    println("${apply_twice(double, 3)}")   // 12

    // Chaining map / filter / reduce
    var nums = [1, 2, 3, 4, 5, 6]
    print(nums.filter((n) => n % 2 == 0).map((n) => n * 10))   // [20, 40, 60]
    print(nums.reduce((acc, n) => acc + n, 0))                 // 21
}

wyn run output of the closures and higher-order-function snippet

105
12
[20, 40, 60]
21

TIP

In today's compiler, lambdas passed to .map/.filter/.reduce take an int parameter. Keep those pipelines integer-valued — e.g. (n) => n * 2.


10. Concurrency — spawn, await, parallel, channels

This is where Wyn stops feeling like a scripting language. spawn f() starts a task and returns a future; await gets its result; await_all waits on a list; and a parallel { } block runs several spawns and joins at the closing brace.

wyn
fn square(n: int) -> int => n * n

fn main() {
    // spawn → future, await → value
    var f = spawn square(9)
    println("await: ${await f}")           // 81

    // await_all over a list of futures
    var futs = []
    for i in 1..4 { futs.push(spawn square(i)) }
    var results = await_all(futs)
    var acc = 0
    for _, r in results { acc = acc + r }
    println("await_all sum: ${acc}")       // 1 + 4 + 9 = 14

    // parallel { } runs bound spawns concurrently, joins at }
    parallel {
        a = spawn square(3)
        b = spawn square(4)
    }
    println("parallel: ${a + b}")          // 9 + 16 = 25
}

wyn run output of the spawn, await, and parallel concurrency snippet

await: 81
await_all sum: 14
parallel: 25

Tasks talk to each other over channels — buffered, FIFO message queues. A classic producer/consumer:

wyn
fn producer(ch: int, n: int) -> int {
    for i in 0..n { Task.send(ch, i * i) }
    return 0
}

fn main() {
    var ch = Task.channel(10)          // buffered channel, capacity 10
    var pf = spawn producer(ch, 5)

    var total = 0
    for i in 0..5 { total = total + Task.recv(ch) }
    await pf
    println("channel total: ${total}")  // 0+1+4+9+16 = 30
}
channel total: 30

11. Modules & the package manager

A Wyn dependency is just a git repo — there's no central registry and nothing to log into (the Go/Deno model). A bare name resolves to github.com/wynlang/<name>; anything with a / is a full repo path.

sh
wyn add args                       # → github.com/wynlang/args
wyn add github.com/bob/cool-lib    # any repo
wyn add github.com/bob/[email protected]   # pin a tag, branch, or commit

wyn add writes a [dependencies] line into wyn.toml and pins the exact commit in wyn.lock for reproducible builds. Import by the short name and call through its namespace:

wyn
import args

fn main() {
    var name = args.get("name")
    println("hello ${name}")
}

Publishing is just as light: push your repo to GitHub and tag a release (git tag v1.0.0 && git push --tags). That's the whole workflow — see Using Packages and Creating Packages.


12. C FFI — the whole C ecosystem, for free

Because Wyn compiles to C, calling C is a one-liner. Declare a function with extern fn (a signature, no body) and call it. The math library links by default:

wyn
extern fn sqrt(x: float) -> float;
extern fn pow(base: float, exp: float) -> float;

fn main() {
    println("sqrt(144) = ${sqrt(144.0)}")     // 12.0
    println("pow(2, 10) = ${pow(2.0, 10.0)}") // 1024.0
}
sqrt(144) = 12.0
pow(2, 10) = 1024.0

For real libraries, wyn add ships curated C recipes. Adding one runs the C bindgen and writes an [ffi] block into your wyn.toml — no GitHub involved:

sh
wyn add m          # libm — the C math library

wyn add m generating 160 extern fn bindings from math.h

Generated 160 extern fn declaration(s) from math.h
Adding C package 'm' — C math library (libm) — sqrt, pow, sin, cos, …
  bound 1/1 header(s) → packages/m/m.wyn
  linked via wyn.toml [ffi]: libs = "m"

✓ Added 'm'. Bindings in packages/m/m.wyn; `import` it or copy the extern fns.

See the full curated list any time with wyn add --list — it includes m, z (zlib), curl, sqlite3, crypto/ssl (OpenSSL), curses, readline, xml2, lz4, zstd, and jsonc. For anything else, wyn bind <header.h> generates bindings from a C header. Details in C FFI.


13. The toolchain

One binary does it all. The commands you'll use daily:

CommandWhat it does
wyn run app.wynCompile and run (fast iteration)
wyn build app.wynNative binary (add --release for -O2)
wyn check app.wynType-check only, no output binary
wyn fmt app.wynFormat in place (--check for CI)
wyn testRun tests in tests/
wyn bench app.wynBenchmark with timing stats
wyn watch app.wynRebuild on save
wyn replInteractive REPL
wyn doctorDiagnose your setup

Formatting. wyn fmt rewrites messy code to the canonical style (4-space indent, no semicolons):

wyn fmt confirming app.wyn was formatted

sh
wyn fmt app.wyn
#   ✓ app.wyn

Building produces a tiny, dependency-free native binary:

wyn build hello.wyn producing a 50KB native binary

sh
wyn build hello.wyn
# ✓ Built: hello (50KB, 364ms)

Benchmarking runs your program many times and reports the distribution:

wyn bench reporting min, avg, median, and p99 timings

sh
wyn bench app.wyn
# Results:
#   min:    25.7ms
#   avg:    141.2ms
#   median: 30.4ms
#   p99:    1146.7ms

Cross-compiling targets another OS/arch from your machine — no Docker, no VM:

sh
wyn cross linux-arm64 app.wyn
wyn cross windows-x64 app.wyn

Targets include linux-x64, linux-arm64, windows-x64, macos-x64, macos-arm64, ios, android, and wasm32. See Cross-Compilation.


14. Capstone — a concurrent prime counter

Time to build a real thing. This program splits the range [0, 100000) across eight workers, counts primes in each chunk in parallel, folds the results, and reports through a struct method. It uses almost everything from this tour: structs and methods, Result + ?, arrays, clos-over loop variables, spawn + await_all, and string interpolation.

wyn
// primes.wyn — a concurrent prime counter.

struct Report {
    limit: int,
    workers: int,
    count: int
    fn show(self) {
        println("Found ${self.count} primes below ${self.limit} using ${self.workers} workers.")
    }
}

fn is_prime(n: int) -> bool {
    if n < 2 { return false }
    var i = 2
    while i * i <= n {
        if n % i == 0 { return false }
        i = i + 1
    }
    return true
}

// Count primes in the half-open range [lo, hi).
fn count_primes(lo: int, hi: int) -> int {
    var c = 0
    for n in lo..hi {
        if is_prime(n) { c = c + 1 }
    }
    return c
}

fn parse_limit(s: string) -> Result<int, string> {
    if s == "" { return Err("empty limit") }
    return Ok(100000)
}

fn main() {
    var limit = parse_limit("100000").unwrap_or(1000)
    var workers = 8

    // Split [0, limit) across the workers and count in parallel.
    var chunk = limit / workers
    var futs = []
    for w in 0..workers {
        var lo = w * chunk
        var hi = lo + chunk
        futs.push(spawn count_primes(lo, hi))
    }

    var results = await_all(futs)
    var total = 0
    for _, c in results { total = total + c }

    var report = Report { limit: limit, workers: workers, count: total }
    report.show()
}
sh
wyn run primes.wyn

wyn run primes.wyn reporting 9592 primes below 100000 using 8 workers

Found 9592 primes below 100000 using 8 workers.

Then ship it: wyn build --release primes.wyn gives you a single native binary you can copy to any matching machine — no runtime, no interpreter, no dependencies.


Where to go next

Or just open the Playground and start typing. Welcome to Wyn.

MIT License — v1.17