Beans is a compiled language for backend services and business systems. I have been building it for a while. This post covers the two things I care about most: how it stays safe, and how it stays fast.
Safety: failure is part of the type
Beans has no null and no exceptions.
If a value can be missing, its type is Option<T>. If a function can fail, it returns Result<T>. You handle it with match, or pass it up with ?.
import std.io
fn parse_qty(text: string) -> Result<int> {
let qty: int = text.to_int()?
if qty <= 0 {
return err("quantity must be positive, got {qty}")
}
return ok(qty)
}
fn main() {
match parse_qty("abc") {
ok(qty) => { io.println("qty {qty}") }
err(e) => { io.println("rejected: {e}") }
}
}
This prints rejected: can't read 'abc' as int.
The part I like most is what happens when you forget. If you try to use the result as a plain number:
let qty: int = parse_qty("5")
the compiler stops you:
error: expected int, got Result<int>
No crash at 3 AM. The mistake shows up at build time.
Raw pointers and raw memory exist, because Beans is also for systems work. But they only work inside unsafe blocks, so you can find every risky line with one search.
Performance: native code, predictable memory
-
Native binaries through LLVM.
beansc build app.b -o appgives you one executable. - Reference counting, not a tracing GC. Memory is freed when the last reference goes away. A cycle collector cleans up loops. There are no stop-the-world pauses.
-
Ownership control when you need it.
movehands a value over without a copy.Shared<T>andWeak<T>make sharing explicit. - Fibers and threads. Fibers for cheap concurrency, plus threads, channels, mutexes, and typed atomics.
I do not publish speed numbers yet. The benchmark suite runs each workload against tuned C++ references, with fixed checksums and a 3% variance limit. A result only counts if it passes on both Linux x86-64 and macOS ARM64. When it does, I will post the numbers, good or bad.
Two engines, one answer
beansc run runs code in an interpreter, so iteration is fast. beansc build makes the native binary. The test suite runs programs both ways and checks they give the same output. It also checks that the compiler, which is written in Beans, rebuilds itself byte for byte.
What you can build today
A web API with Espresso: controllers, constructor injection, middleware, OpenAPI, on a non-blocking HTTP/1.1 event loop.
import github.com/beans-lang/espresso
@espresso.controller(route: "/hello")
pub class HelloController extends espresso.Controller {
pub fn init() {}
@espresso.get(route: r"/{name}")
pub fn hello(@espresso.route name: string) ->
Result<espresso.ActionResult> {
return self.ok_text("Hello, {name}!")
}
}
Even here, the action returns a Result. Errors stay values all the way up.
Also in the ecosystem:
- Latte: server-rendered UI components that can also run in the browser as WebAssembly.
- Cortado: native desktop apps.
- Drivers for PostgreSQL, MySQL, SQLite, and Redis.
Where it stands
Beans is pre-1.0 (v0.1.52). Things can still change. I am looking for people to build a small real tool with it and tell me what breaks.
If you try it, I would like to hear what felt wrong.
Top comments (4)
The interpreter/native output comparison is a useful baseline. I'd extend it with an Espresso handler that owns a resource, returns early through ?, and has a destructor write a marker. Compare the error result and exactly-once cleanup in both engines, rather than stdout alone. An integer-overflow boundary in the same suite would also make the failure semantics explicit. Are those error-path fixtures already part of the differential tests?
Thank youβthat is a useful distinction. We already have a compiler-level fixture which involves acquiring a resource, in which a later ? returns an error and the destructor prints a marker. The test verifies interpreter/native parity against the expected output, including the cleanup carried out before the caller reports the error.
This is a cleanup test. github.com/beans-lang/beans/blob/m...
That deals with the language behaviour; it would be the responsibility of your dedicated Espresso handler to add coverage in relation to integration. With regard to arithmetic, addition, subtraction, and multiplication for fixed-width integers result in wrapping, whereas decimal overflow causes a panic. Boundary tests should make those different semantics clear.
This is the language contract. github.com/beans-lang/beans/blob/m...
Some comments may only be visible to logged-in visitors. Sign in to view all comments.