A low-level compiled language for people who want C's control without giving up their afternoon to memory bugs.
Why? • Language Tour • Self-Hosted • Getting Started • Join Us
Luma is a systems programming language built around one bet: you can catch most of the memory bugs that matter at compile time without a borrow checker, without lifetimes, and without a garbage collector.
Memory management stays manual. You call alloc() and free() yourself, same as C. What's different is that the compiler watches you do it. As part of type checking, Luma's static analyzer tracks ownership through your code and flags use-after-free, double-frees, and leaked allocations before you ever get to run the program.
Luma doesn't pretend to be memory-safe. Out-of-bounds access, uninitialized reads, and raw pointer misuse are all still on you. What it does promise is that the specific, common mistake of losing track of an allocation gets caught early, for free, with syntax you can read at a glance.
Most languages ask you to pick a side: manual memory management with C, or safety with something that takes memory management away from you. Luma's trying to split the difference a little differently:
- Ownership hints instead of lifetimes. Annotate a function with
#returns_ownershipor#takes_ownershipand the analyzer understands who's responsible for freeing what, no lifetime syntax anywhere in your code. - No runtime cost. All of this happens at compile time. There's no garbage collector, no reference counting, nothing running behind your back.
- Small, direct syntax. Control flow and memory operations are always visible in the source, nothing is implicit.
It's not trying to out-safety Rust. It's trying to give you most of the "wait, did I free that?" coverage without asking you to learn an entirely new mental model to get it.
A few of the things Luma actually looks like, pulled straight from the test suite so they're guaranteed to compile.
Structs and methods:
const Point -> struct {
pub:
x: float,
y: float,
distance_to -> fn (other: Point) float {
let dx: float = other.x - self.x;
let dy: float = other.y - self.y;
return sqrt(dx * dx + dy * dy);
},
};
Struct embedding, for when composition beats inheritance-shaped code:
const Entity -> struct {
pub:
x: float,
y: float,
move -> fn (dx: float, dy: float) void {
self.x = self.x + dx;
self.y = self.y + dy;
},
};
const Player -> struct {
pub:
...Entity,
health: int,
};
let p: Player = Player { x: 0.0, y: 0.0, health: 100 };
p.move(1.0, 1.0); // promoted straight from Entity
Scoped switch, so you're not writing Color::Red on every arm:
switch using Color (c) {
Red -> { output("stop\n"); }
Green -> { output("go\n"); }
}
Ownership annotations, so the analyzer knows what a function does with what you hand it:
#returns_ownership
const make_counter -> fn () *int {
let p: *int = cast<*int>(alloc(sizeof<int>));
*p = 0;
return p;
}
#takes_ownership
const print_and_free -> fn (counter: *int) void {
output(*counter, "\n");
free(counter);
}
FFI, straight to a C library, no bindings generator involved:
@link("libc.so.6")
pub const malloc -> fn (size: int) *void;
That's a small slice. The full language reference is in docs/docs.md.
The compiler is written in Luma. It compiles itself, and every commit proves it can: an existing luma binary builds the current compiler source, that output builds the same source again, and the two results are diffed byte-for-byte. If a compiler can't reproduce itself exactly from its own source, something's wrong, so that check runs before anything else does, on every push and every PR.
Releases are cross-compiled from that same self-hosted compiler: a single Linux CI run produces Linux, Windows, and macOS binaries, and the Windows/macOS ones actually get downloaded and executed on real runners before a release goes out. See docs/releases/ for what's shipped and when.
Latest release: v0.3.5
What's working:
- Full lexer, parser, type checker, and C-transpiling codegen, self-hosted
- Static ownership analysis: use-after-free, double-free, and leak detection
- Structs, enums, struct embedding, static methods, scoped
switch - FFI via
@link(any C/POSIX shared library) and#dll_import(Windows DLLs) - A language server (
luma --lsp) with diagnostics, hover, and completion - Cross-platform builds for Linux, Windows, and macOS, verified in CI
What's not there yet: generics, and a few rough edges in the static analyzer around conditional allocation paths. See the Known Limitations section of the latest release notes for the current honest list.
Building from source just needs a C compiler no LLVM, no Meson, nothing else to install first:
git clone https://github.com/Luma-Programming-Language/Luma.git
cd Luma
./scripts/bootstrap-build.sh
sudo ./scripts/install.shbootstrap-build.sh handles the chicken-and-egg problem of a self-hosted compiler for you: it ships with a prebuilt seed binary, uses it to build the current source, then rebuilds itself with its own output and checks the two match before calling it done.
Prefer a prebuilt binary? Grab one from the latest release instead.
@module "main"
pub const main -> fn () int {
output("Hello, World!\n");
return 0;
}
$ luma hello.lx -name hello
$ ./hello
Hello, World!luma can target Windows from Linux directly, as long as mingw-w64 is installed (mingw-w64-gcc on Arch, gcc-mingw-w64-x86-64 on Debian/Ubuntu):
luma main.lx -t windows64 -name main.exemacOS doesn't have an equivalent cross-compiler — -t macos on Linux will emit correct macOS-flavored C, but there's nothing on Linux that can link it into a working binary. Use -c/--no-compile to stop after emitting the C, and hand that off to an actual Mac's own cc:
luma main.lx -t macos -c -name main # writes output/main.c, doesn't try to link it
# ... transfer output/main.c to a Mac ...
cc output/main.c -lm -o mainBuilt with ❤️ by the Luma community
