Willow programming language
Github link:
https://github.com/lechatthecat/willow
Willow is an experimental statically typed programming language that compiles to native code through Cranelift. It has class-based OOP, algebraic enums, pattern matching, Option and Result, garbage collection, and a stackless async runtime with tasks, channels, cancellation, and select.
I have been building it mostly as a one-person project, partly to understand how compilers, garbage collectors, and schedulers interact when they are designed together rather than as isolated components.
Willow is not production-ready. But it has reached the point where it can run non-trivial programs, compile multiple modules, and be benchmarked meaningfully against established runtimes.
Why I built it
Most of the languages I like make a different trade-off somewhere: Go keeps the language and tooling simple but has limited traditional OOP; Java has a mature managed runtime but depends on a large VM; Rust gives very strong control and performance but requires explicit ownership reasoning.
Willow is an experiment in a different combination: native AOT compilation, automatic memory management, familiar OOP, algebraic data types, and lightweight structured concurrency.
Willow code
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Dot,
}
fn area(shape: Shape) -> f64 {
return match shape {
Shape::Circle(r) => r * r * 3.14159,
Shape::Rectangle(w, h) => w * h,
Shape::Dot => 0.0,
};
}
async fn compute() -> f64 {
await sleep(1);
return area(Shape::Circle(5.0));
}
async fn main() {
let task = compute();
println(await task);
}
Classes and interfaces are also first-class language features:
interface Animal {
fn speak(self) -> String;
}
class Dog implements Animal {
pub fn speak(self) -> String {
return "woof";
}
}
fn describe(animal: Animal) {
println(animal.speak());
}
fn main() {
describe(new Dog());
}
Willow supports match
fn heading(d: Direction) -> String {
return match d {
Direction::North => "north",
Direction::East => "east",
Direction::South => "south",
_ => "west"
};
}
defer is also supported.
async fn scalar(fail: bool) -> i64 {
let mut count = 7;
if true {
defer match recover() {
Some(info) => println("scalar: " + info.message),
None => println("scalar: clean")
}
await sleep(1);
if fail {
panic("no count");
}
count = 100;
}
return count;
}
Result, Option are also supported.
null or nil don't exist in Willow, but Option::None exists instead.
fn require(value: Option<i64>) -> i64 {
let mut result = -1;
if true {
defer match recover() {
Some(info) => println(" message: " + info.message),
None => {}
}
result = value.unwrap();
}
return result;
}
Runtime
The runtime is not built on Tokio, async-std, or another host-language scheduler. Willow has its own task scheduler, channels, cancellation model, and garbage collector.
The GC is generational. Major marking is concurrent, while minor collections still use stop-the-world collection. Async frames are GC-managed, and their layouts are derived from compiler liveness information.
Performance
The numbers are in milliseconds.

Willow has lighter task footprint: 814.8 B/task vs Go 2,742.4, Java 1,289.1.
Fibonacci:
Willow can already compete with Go in this microbenchmark.
Object churn:
Go is still tens of times faster.
Virtual dispatch:
Willow still pays far too much per method call.
Channels:
The abstraction is working, but the underlying task/channel path remains expensive.
Willow is still experimental. The standard library is small, several runtime paths are much slower than Go or Java, tooling is incomplete, and neither the language nor its ABI should be considered stable yet.
Future
The next area I want to explore is tooling designed for coding agents.
Instead of making the language syntax "AI-friendly," I want the compiler to expose semantic facts directly. One planned feature is a semantic blast-radius query:
Level 1: code directly changed
Level 2: direct callers/users
Level 3: callers of those callers
effect changes such as newly introduced blocking, suspension, allocation, or panic paths
A more ambitious version could detect that a changed function is reachable from a retry loop and that the change introduced a new side effect, then surface a review question such as:
“Could this side effect execute twice if the operation succeeds but its acknowledgement is lost?”
Top comments (0)