The signature stops being a suggestion.
In most languages, a function can quietly open a file or call out over the network, and nothing in its description warns you it does. BuildLang asks each function to declare it out loud. A function lists exactly what it’s allowed to touch; if it ends up reaching for something it never put on that list, the program is turned away, and the error points right at the line that broke the promise. Memory is treated the same gentle way: which piece of memory comes back to you, and how long it’s allowed to live, is checked rather than hoped for.
Two honest questions every function should answer (what am I allowed to touch, and whose memory am I handing you), answered by the compiler that runs the code, not by a comment that hopes you read it.Don’t take the type. Watch it refuse.
Each of these is a real file checked by the real binary: buildc 1.0.5, release build. The output is verbatim.
fn load_config() { read_file("ops.toml"); // reaches the filesystem } $ buildc check missing_effect.bld function `load_config` performs effect `FileSystem` but does not declare it help: add ~ FileSystem to the signature note: capability `FileSystem` was triggered by ambient call(s): read_file # declare the effect, and the same check passes, then writes a receipt fn load_config() ~ FileSystem { read_file("ops.toml"); } $ buildc check load_config.bld --receipt receipt.json Type checking... OK "declared_effects": { "load_config": ["FileSystem"] } "observed_capabilities": { "load_config": { "FileSystem": ["read_file"] } }
Touch your files without saying so, and it won’t build; the error points at the exact line that did it. Declare it instead, and it passes, and the compiler writes down what you promised, right next to what it actually saw you do.
Captured live from buildc check (Rust toolchain, release build) · the effect is built into the function and checked every time it’s called. That little receipt is the heart of it (what you promised, side by side with what really happened), and it’s honest that this is the foundation, not a finished, ship-it-tomorrow product.
fn pick<'a, 'b>(x: &'a i32, y: &'b i32) -> &'a i32 { y // returns 'b where the signature promised 'a } $ buildc check pick.bld Lexing... OK (28 tokens) Parsing... OK (1 items) Type errors found: function returns `&'b i32` but expected `&'a i32`
Here’s what’s happening: the function promised to hand back one specific piece of memory, then tried to hand back a different one that might disappear sooner. The compiler catches the swap and says no. How long memory is allowed to live is a promise too, and it’s kept.
Diagnostic from compiler/src/types/error.rs, surfaced through the lifetime matching in unify.rs. An honest limit, said plainly: this memory-lifetime checking is not yet complete. It already follows memory as it passes between functions (that’s the Phase 1 work), but I haven’t finished proving every case at the caller’s side, so I won’t claim it’s complete.
# an algebraic effect with a handler: parsed, effect-checked, # lowered to C, compiled, and run: $ buildc run examples/quickstart/effects_greeting.bld Hello, teammate!
That same promise-keeping follows the program the whole way down: read it, check what it’s allowed to touch, translate it into the C language, and run it. From there, one path turns it into a real program your machine can run on its own.
The C path is the one that works start to finish, and the tests prove it. The others (LLVM, WASM, SPIR-V, x86-64, ARM64, and a route that writes Rust) can produce code, but they’re still experiments. I’d rather just tell you that than let you assume otherwise.
# name a C library by its header and link flag, then call straight in: extern "C" link "sqlite3" header "<sqlite3.h>" { fn sqlite3_libversion() -> &str; // the header stays authoritative } fn main() ~ Foreign { let v = sqlite3_libversion(); } $ buildc build examples/ffi --emit c [4/4] Code generation (c)... OK (53517 bytes) # in the emitted C, verbatim: #include <sqlite3.h> // your header, included as-is // buildc-link: sqlite3 // recorded, so `buildc build` links it _1 = sqlite3_libversion(); // the real call into the C library # the reverse is `extern "C" fn` (C calls BuildLang). and a live run # straight through a C variadic: $ buildc run examples/variadic/main.bld the answer is 1 and 2
Name a C library by its header and link flag, and BuildLang includes the header, records the link, and calls straight in. extern "C" fn goes the other way, so C can call back. A foreign call is still an effect: main declares ~ Foreign, so reaching into C sits behind the same gate as touching a file.
Captured live from buildc build --emit c and buildc run (release build). buildc emits the include, the link directive, and the call, checked end to end through the build; the final native link against sqlite3 is your own C toolchain’s job, not something the test suite claims for you.
More than 1,300 passing tests, and an honest list of what doesn’t ship yet.
Built in Rust. The Cargo suites pass green at the current 2026-06-30 baseline: 872 library, 263 CLI, 83 parser, 51 lexer, and 44 binary tests, 0 failed (the suites were consolidated since the older single 1002 count, so the totals aren’t a straight comparison). The C path carries a program all the way to something your machine can run on its own, and it’s the route the tests check from one end to the other. That’s the whole of what I’ll claim, and nothing the tests won’t back up.
It isn’t winding down, and it isn’t standing still. The core holds steady while the ground underneath it keeps deepening.
The core is shipped and steady: say what you’ll touch, be clear about your memory, checked the whole way down to native code. That’s solid ground, and lately the ground itself has been growing. BuildLang now speaks C both ways: declare a C library with its header and link flag and call straight into it, or expose a function as extern "C" so C can call back, all emitted and checked through buildc build. Linear types landed too, an opt-in no-cloning rule that’s the shared keystone for quantum, financial, and on-chain work (experimental, not yet fully sound, and I say so plainly). The first real step toward automatic memory cleanup is in as well, opt-in and deliberately narrow for now. I’d still rather you hear the rough edges from me than stumble on them: doing many things at once is parsed and type-checked, but its runtime isn’t wired into compiled programs yet; the compiler meant to one day build itself is drawn up but doesn’t run; and the experimental backends can make code but aren’t ready for real use. I built this on my own. These are the named limits of a foundation that’s still deepening, not a project closing its doors.
Build it. Then make it reject something.
The quickest way is cargo install buildlang from crates.io; you’ll also want a C compiler installed where the system can find it. Or build from source. Either way, buildc doctor tells you the C path is good to go. Point it at the examples, or, better, write a function that fibs about what it touches, and watch it gently refuse.
$ cargo install buildlang # from crates.io; installs the buildc binary # or from source: $ git clone https://github.com/HarperZ9/buildlang && cd buildlang/compiler $ cargo build --release $ buildc doctor # toolchain + C-backend readiness C backend: ready $ buildc run examples/quickstart/effects_greeting.bld Hello, teammate!