Skip to content

Wyn v1.19: The DX Release

The last two releases built the ecosystem and then hardened it. This one is about what it feels like to use Wyn every day: the test runner, the project templates, the error messages, the upgrade path. And underneath the DX banner, two hardening passes that fixed every known silent-wrong-answer bug in the compiler and put it under continuous fuzzing and sanitizer gates.

No breaking changes. This is the largest release yet, so here are the parts you'll actually feel.

No more silent wrong answers

A compiler bug that crashes is annoying. A compiler bug that produces the wrong value is dangerous. This release hunted the second kind.

Structs compare with ==. Day-one code, and it used to be an internal compiler error:

wyn
struct Point { x: int, y: int }

fn main() {
    a = Point{x: 1, y: 2}
    b = Point{x: 1, y: 2}
    if a == b { print("equal") }
}

Comparison is field-wise: strings by content, nested structs recursively. Structs with non-comparable fields (arrays, maps), cross-type compares, and ordering with < give clear check-time errors instead of garbage.

Channels are typed. A channel's element type is inferred from its first send and enforced from then on:

wyn
fn main() {
    ch = channel(4)
    ch.send("hello")
    msg = ch.recv()
    print(msg)                 // hello, not a pointer number

    cf = channel(4)
    cf.send(3.14)
    print(cf.recv())           // 3.14, not 3
}

Yes, recv() on a string channel used to print a pointer, and floats came back truncated. Sending the wrong type is now a check-time error.

Also in the wrong-answer purge: mismatched collection stores (pushing a string onto an int array used to pass wyn check and corrupt memory) are check-time errors; awaiting a future twice returns the same memoized value instead of 0 or, worse, a different spawn's result; nested Options (Some(Some(x))) compile and match; and select dispatches correctly at every arity, where one-arm and 4+-arm selects used to always run the first arm. Or-patterns in match deserve a special mention: 1 | 3 | 5 => compiled to if (0), meaning the arm silently never matched. Now:

wyn
fn describe(n: int) -> string {
    return match n {
        1 | 3 | 5 => "small odd",
        x if x > 100 => "big",
        _ => "other",
    }
}

Or-patterns match, guards no longer crash the compiler, and range patterns 1..5 / 1..=5 parse.

String lambdas: the gap is closed

v1.18 shipped an honest "not supported yet" error for string lambdas. v1.19 ships the feature. Lambda bodies now go through the real expression code generator instead of a separate int-only mini-emitter, so every expression form works inside a lambda:

wyn
fn main() {
    words = ["hello", "wyn", "world"]
    loud = words.map((s) => s.upper())
    long = words.filter((s) => s.len() > 3)
    greetings = words.map((s) => "Hi " + s + "!")
    print(loud)        // ["HELLO", "WYN", "WORLD"]
    print(long)        // ["hello", "world"]
    print(greetings)   // ["Hi hello!", "Hi wyn!", "Hi world!"]
}

Element types are preserved through the chain: .map() on a [string] returns [string]. This was the biggest expressiveness gap in the language, and I'm glad it's gone.

A real test runner

wyn test now discovers tests/test_*.wyn and *_test.wyn, runs them, and, crucially, failing assertions fail the run. They used to exit 0 and count as passed, which is worse than no test runner at all. wyn test <name> filters by name, and an empty project prints a copy-pasteable starter test instead of a shrug.

Alongside it, wyn new <name> --template cli|api|web|lib scaffolds a runnable project: wyn.toml, src/, a passing test, README, CI workflow. Embarrassing admission: the templates had shipped containing the &&/|| syntax that v1.17 removed, so they didn't compile. They do now, and every template is CI-verified to check clean and pass its own tests, so that can't regress quietly.

Supply-chain auditing

Since a Wyn dependency is a git repo, the supply-chain question is "did the repo change under me?" wyn pkg audit answers it: a moved tag (the classic retag attack) is an error, branch pins warn with the exact command to pin today's SHA, local cache tampering is detected, and dependencies that link native code via [ffi] are flagged for review. --offline skips remote checks; the exit code is the worst finding, so it slots into CI.

The parser can't be crashed

A fuzzing harness now runs in make test and CI. Every crash and hang it found is fixed: four parser infinite loops, a segfault on ===, a UTF-8 BOM silently emptying entire programs, split("") hangs, .chars() corrupting multi-byte UTF-8, and more. The invariant going forward: 0 crashes, 0 hangs, 0 internal errors on malformed input.

Error messages got the same treatment. Unterminated strings point at the opening quote (one error, not a cascade of three). And Python and JS habits get targeted fixes: def, lambda, True, null, let, console, try each produce the exact Wyn equivalent instead of a generic parse error.

Two small forgiveness wins I use constantly: return x auto-wraps in fn -> T? functions, no more Some(x) ceremony:

wyn
fn find(key: string) -> int? {
    if key == "answer" { return 42 }
    return none
}

fn main() {
    print(find("answer").unwrap_or(0))    // 42
}

And unknown methods on strings and arrays are rejected at check time with a "Did you mean: .upper()?" hint; they used to pass wyn check and die in the C compiler.

Self-upgrade that can't hurt you

wyn upgrade updates to the latest release; wyn upgrade 1.18.0 installs an exact version, up or down. The new binary is downloaded to a temp directory, extracted, and proven to run before anything is touched. Previously a 404 response body could overwrite your working binary, which is the kind of bug you only get to ship once.

The release the canary caught

A story worth telling. This release added an install-layout canary to CI: a gate that installs the actual tarball via install.sh and runs it, the way a user would, before the release is allowed out.

On the first v1.19.0 tag, it went off. The binary worked as ./wyn but not as wyn through PATH: it resolved its runtime relative to argv[0], which is not the executable's real location when the shell finds it via PATH. Every test suite passed, because test suites run ./wyn. The fix (resolve from the real executable path) went in, the tag was moved, and the release you can download is the retagged one.

I built the canary because "works in CI, broken when installed" is the failure mode test suites are structurally blind to. It paid for itself on day one. The TSan gate did too, catching a real data race in select before release: every commit now runs the full suite under AddressSanitizer and ThreadSanitizer, under both executor configurations.

Ecosystem and performance, briefly

wyn pkg add web brings the first official batteries-included web package: request parsing, JSON/HTML responses, traversal-safe static files, spawn-per-request concurrency. With this release's HTTP/1.1 keep-alive and concurrent-accept work it measures 22,000+ req/s with zero failed requests at 200 concurrent connections on the hello example, 3x the previous throughput.

Curated C recipes went from 12 to 31, and wyn bind now handles real-world headers properly: typedef resolution, macOS availability attributes, macro-generated APIs. Measured on real libraries: png 0 to 232 bound functions, pcre2 0 to 212, OpenSSL crypto 729. Multi-package projects link now too; previously only the last-added package's [ffi] libs survived.

Compile speed: dev builds are ~40% faster on macOS (the 5,400-line runtime header is precompiled once instead of re-parsed per build; hello went 830ms to 500ms), wyn check on very large files is near-linear instead of O(n²), and compiled binaries dropped a 2.1MB zeroed scheduler block from their data segment.

Upgrading

bash
wyn upgrade          # from 1.18.0 or later
wyn --version        # 1.19.0

146 expect/regression tests plus 20 dedicated sub-suites, the fuzz invariants, and the ASan/TSan sets, all green on all platforms. No breaking changes.


Browse the version history.

MIT License - v1.19.1