A systems language that
gets out of your way.
Memory safety, deterministic cleanup, and compile-time data race prevention. No garbage collector. No lifetime annotations. A mental model small enough to hold in your head.
What we believeWhat Quack is for
Every design decision flows from these principles.
- The language should disappear If you're thinking about Quack instead of your problem, the language has failed. Every syntax, every rule, every diagnostic is evaluated against this.
- Reject, don't tax When the compiler can't prove safety, it rejects the code and explains why. It never silently inserts allocations. But it also doesn't demand ceremony that carries no information.
- One obvious way One ownership model. One error model. One concurrency model. Measured from the programmer's intent, not the compiler's internals. If a newcomer can't read it, redesign the syntax.
- Diagnostics are part of the language Every rule must produce a clear, actionable error message. If a rule can't explain itself simply, the rule is too complex and must be redesigned.
- Make costs visible, not ceremony If it costs something and you have a choice, it's visible in syntax. If it's free, or if there's no alternative, it's silent. Cognitive cost counts too.
What that means in practice
Capabilities, not lifetimes Three words describe how every function uses its arguments: read for shared read-only access, mut for exclusive mutation, take for ownership transfer. The compiler checks the rest. You write zero lifetime annotations — not fewer, none. | Errors are return types Config or IOError or ParseError — the return type is the error contract. return for success, error for failure. No wrappers, no exceptions, no hidden control flow. |
Any function, any thread Any function can run concurrently — you don't mark functions as special. No async/await, no splitting your codebase into two worlds. Green threads and a runtime scheduler handle the rest. | Words over symbols and/or/not instead of &&/` |
No null Absence is T or None — a union type. The compiler won't let you use the value until you've checked which type it actually is (narrowing). No null pointer dereferences, ever. | Native performance Compiled to native code via LLVM. Memory is freed the moment ownership ends — no garbage collector, no pauses, no latency spikes. |
What it looks like
Ownership without lifetime annotations
Rust needs lifetime parameters to track how long references live. Quack infers it.
fn first_word<'a>(s: &'a str) -> &'a str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}func first_word(s: String) read String {
match s.find(' ') {
i: Int then return s.slice(0, i),
none then return s,
}
}
Rust's 'a annotation proves the returned reference doesn't outlive the source string. Quack's read carries the same guarantee — the compiler tracks the relationship internally. Zero lifetime annotations, not fewer.
Error handling without boilerplate
Go checks errors explicitly but the pattern is repetitive. In Quack the return type declares every possible outcome.
func loadConfig(path string) (Config, error) {
text, err := os.ReadFile(path)
if err != nil {
return Config{}, err
}
var cfg Config
err = toml.Unmarshal(text, &cfg)
if err != nil {
return Config{}, err
}
return cfg, nil
}func load_config(path: String) Config or IOError or ParseError {
let text = read_file(path) otherwise pass back
let config = Toml.parse[Config](text) otherwise pass back
return config
}
The return type is the error contract — Config or IOError or ParseError says exactly what can come back. No wrapper types, no Ok()/Err(). otherwise pass back does what Go's three-line if err != nil block does — in three words.
Concurrency without async/await
Rust marks functions async and splits your codebase in two. Go gives you goroutines but no compile-time data race prevention. Quack uses green threads with structured concurrency — any function can run concurrently, and the compiler rules out data races at compile time.
async fn fetch_all(urls: Vec<String>) -> Result<Vec<Response>, Error> {
let futs = urls.into_iter().map(|u| tokio::spawn(fetch(u)));
let results = join_all(futs).await;
results.into_iter().map(|r| r?.map_err(Error::from)).collect()
}func fetchAll(urls []string) ([]Response, error) {
var wg sync.WaitGroup
var mu sync.Mutex
results := make([]Response, 0)
var firstErr error
for _, u := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
r, err := fetch(url)
mu.Lock()
defer mu.Unlock()
if err != nil {
firstErr = err
} else {
results = append(results, r)
}
}(u)
}
wg.Wait()
return results, firstErr
}func fetch_all(urls: Vec[String]) Vec[Response] or Error {
let results = Mutex.new(Vec[Response]())
parallel(p) {
for url in urls {
p.spawn {
let r = fetch(url) otherwise pass back
results.lock().push(r)
}
}
}
return copy results.lock()
}
No async/await. Any function can suspend — you don't split your codebase into sync and async halves. parallel(p) scopes all tasks to the block; children cannot outlive their parent. The compiler rejects shared mutable state without synchronisation.
Quack narrows expressiveness to shrink the mental model. Some patterns are intentionally inexpressible. The tradeoff: you can hold the entire language in your head, and the compiler handles the verification.
- No use-after-free
- No data races
- No null
- No GC
- No lifetime annotations
- No async/await split
- Deterministic cleanup
Where we are
The bootstrap compiler (written in Rust) compiles Quack programs to native binaries via LLVM. The core language works: ownership, capabilities, error handling, pattern matching, generics, interfaces, and multi-file projects. 106 of 124 validation programs compile and run correctly.
Current work: completing the type system (reflection, derive, shared reference counting) and closing correctness gaps in generic code and collection ownership. The concurrency runtime and standard library come after the type system is solid.
The compiler, specification, and all documentation will be open-sourced when the repo goes public.