Wyn v1.17: The Ecosystem Release
This is the big one. v1.16 opened a door to the C ecosystem with a working extern fn. v1.17 walks through it, and brings a package manager along.
I'll be upfront: this release also removes things. The |> pipe operator, &&/||/!, and try/catch/throw are gone, and import m now requires qualified m.foo() calls. The migration is mechanical, and I'd rather make these cuts now, while the ecosystem is one week old, than in five years when they'd break the world. Details at the bottom.
A package manager with no registry
I spent a while designing a package registry before realizing I didn't want one. A registry is a server to run, an account system to secure, a namespace to squat, a single point of failure. Wyn's answer: a dependency is a git repo.
wyn add args # expands to github.com/wynlang/args
wyn add github.com/bob/[email protected] # any repo, any host, optional tag
wyn add [email protected]:team/lib --as corplibDependencies land in wyn.toml [dependencies] and import <name> resolves through it. Clones go to a global content-addressed cache (~/.wyn/pkg/<host>/<owner>/<repo>@<ref>), shared across projects, pinned by commit in wyn.lock for reproducible builds. wyn remove, wyn list, and wyn install (restore from the lockfile) round it out.
Publishing a package is git push and git tag. That's the whole ceremony. Private repos work automatically because the compiler shells out to git clone, so whatever auth your git already has (credential helper, SSH agent) just applies.
The pkg.wynlang.com registry and its wyn pkg register/login/publish commands are removed. There is nothing to register on and nothing to log into, which is the point.
C libraries in one command
v1.16 gave you extern fn and manual [ffi] linking. v1.17 automates the whole path. For a curated C library, one command resolves it, generates its bindings, links it, and records the flags:
wyn add sqlite3Nine recipes ship: m, z, curl, sqlite3, crypto, ssl, curses, readline, and xml2. Run wyn add with no name to browse them in a small TUI. For everything else, wyn bind header.h reads a C header and emits the extern fn declarations for whatever the FFI type map can represent.
extern fn sqlite3_libversion() -> string;
fn main() {
print("SQLite", sqlite3_libversion())
}The piece that makes real C APIs usable is Ptr, a heap cell for out-parameters. When C wants a sqlite3**, you pass Ptr.cell() and read the handle back with Ptr.read(). I dogfooded this against SQLite end to end: open a database, create a table, insert rows, query them back through prepared statements, all from Wyn. It works, and the C-FFI guide walks through it.
Coroutines are now the default
Until now, only fire-and-forget spawn ran on the coroutine scheduler; awaited spawns went to a thread pool with blocking I/O. That split is gone. Awaited spawn, await_all, and parallel { } all run as coroutines on an M:N scheduler, so cooperative I/O and Time::sleep engage everywhere:
fn fetch(id: int) -> int {
Time::sleep(10)
return id * 100
}
fn main() {
f1 = spawn fetch(1)
f2 = spawn fetch(2)
print(await f1, await f2) // 100 200
}Awaiting from main pumps the scheduler and never deadlocks. The old thread pool remains behind WYN_ASYNC_POOL=1 if you need it.
New in the same pass: cooperative task cancellation. Task.cancel(h) requests it; the task checks Task.is_cancelled() and bails:
fn worker() -> int {
var i = 0
while i < 1000000 {
if Task.is_cancelled() { return -1 }
i = i + 1
}
return i
}
fn main() {
h = spawn worker()
Task.cancel(h)
r = await h
print("worker returned", r)
}Being honest about the semantics: this is cooperative and leak-on-cancel. There is no forced unwind; a task that never checks the flag never stops. That's a deliberate first step, not the final design.
print() grows up
print used to be quietly inconsistent: no newline for strings and ints, a newline for arrays, and a silently dropped second argument. Now it's Python-style for real:
fn main() {
name = "Wyn"
version = 17
print("hello", name, version) // hello Wyn 17
print("no", "newline", end: "")
print("")
print("a", "b", "c", sep: "-") // a-b-c
}Space-separated arguments, trailing newline, end: and sep: keywords. println stays as an alias, so nothing breaks.
Smaller but pleasant
Optional chaining short-circuits on None:
struct Config { host: string }
fn find_config(env: string) -> Config? {
if env == "prod" { return Some(Config{host: "prod.example.com"}) }
return None
}
fn main() {
cfg = find_config("prod")
host = cfg?.host
print(host)
}zip() with pair destructuring:
fn main() {
names = ["ada", "bob", "cyd"]
scores = [97, 82, 91]
for name, score in zip(names, scores) {
print(name, score)
}
}Plus optional struct fields, closure copies, braceless single-statement bodies, _ as a throwaway loop variable, and variables named after C keywords. Under the hood, three memory-safety bugs were fixed and ASan-verified.
The breaking changes
|>removed. Compose with nested calls or intermediate variables.&&/||/!removed.and/or/notwere already canonical; now they're the only spelling.try/catch/throwremoved. Errors are values:Result,Ok/Err,match, and?.import mrequiresm.foo(). The collision-free form is now the only form;import { foo } from mkeeps flat calls when you want them.- The registry client is gone, as above.
Every one of these produces a clear compile error pointing at the replacement. If you hit something the error message doesn't explain, that's a bug; tell me.
Upgrading
wyn --version # 1.17.0Run your build, follow the errors, and you're done; the fixes are one-liners.
Browse the version history.