Bbetlang

Features

Error handling, modules, concurrency, and FFI.

Cross-cutting features that span several keywords. The memory model has its own page (Arenas); functions, methods, and generics are on Functions.

Error handling (Go-style values)

bet has no exceptions and no interest in getting any. Errors are ordinary values, returned as the last element of a multi-value return ((T, yikes)). The nil error, the one that means nothing went wrong, is the keyword ghosted.

  • yikes: the error type. yikes.new("msg") constructs one; err.tea("context") wraps one with added context, because every good error deserves a little tea.
  • ghosted: the nil (no-error) value. The core idiom is fr y != ghosted { ... }.
  • bounce err: early-return sugar. If err is a real error, it returns the zero values plus that error, which collapses the usual three-line check down to one word.

bounce is for getting out clean, and I’ve pulled this off a number of times, but between my lawyers and the statute of limitations, the wedding is the only one I’m cleared to talk about. I really did not want to be there. The whole plan was: shake hands with everyone standing closest to the bride and groom, get into one photo clearly having the time of my life, and then announce, to nobody in particular, that I was running out to my car to grab my camera. I did not own a camera. I owned a car, and I used it. bounce is that move with a return type. The instant the value coming back is a real error, it hands the error up the chain and gets you out of the function before anything else has to happen. Obligations met on paper, exit made in practice.

finna half(n: int) -> (int, yikes) {
    fr n % 2 != 0 {
        bet 0, yikes.new("not even")
    } naw {
        bet n / 2, ghosted
    }
}

finna pipeline(n: int) -> (int, yikes) {
    lowkey a, y = half(n)
    bounce y                    // if y != ghosted, return (0, y) now
    lowkey b, y2 = half(a)
    bounce y2
    bet b, ghosted
}

Panic and recover (yeet / sheesh)

For the genuinely unrecoverable stuff (rare, and the docs would rather you didn’t): yeet("message") panics, throwing the whole thing overboard, and a sheesh { ... } naw p { ... } block is a recover boundary that catches the panic and binds it. For anything you actually expect to handle, reach for a (T, yikes) return instead.

Modules (pull)

pull "name" imports a module. A stdlib namespace (spill, str, and so on) resolves to intrinsics; a path resolves to a sibling .bet source file, relative to the importer.

  • Only flex (exported) items cross the import boundary; hush (the default) items stay module-private.
  • pull "shapes/geometry" as geo: a subdirectory path plus an as alias that renames the namespace.
// geometry.bet
flex facts UNIT: int = 10
flex drip Point { flex x: int, flex y: int }
flex finna area(w: int, h: int) -> int { bet w * h }
finna scale(v: int) -> int { bet v * UNIT }     // hush (private) — not exported

// main.bet
pull "geometry"
finna main() {
    spill.it(geometry.area(3, 4))                 // 12
    lowkey p: geometry.Point = geometry.Point{ x: 2, y: 5 }
}

Concurrency (slide)

slide fn() spawns a concurrent task (like Go’s go). You tell a function to slide into its own thread and go do its thing.

finna worker() { }

finna main() {
    slide worker()
    spill.it("main")
}

Current limitation: slide targets a no-argument named function today. Cross-thread sharing of cribs and holla is not yet part of the model.

FFI (extern)

extern "C" finna name(params) -> ret declares a foreign C symbol with a bet signature, resolved at link time. This is how betlang talks to the enormous pile of C the world already runs on. Pointer arguments use the rawptr type.

extern "C" finna abs(x: i32) -> i32

finna main() {
    spill.it(abs(-7 as i32))   // 7
}