Magma is a small, low-level, compiled language that currently lowers to LLVM IR. It is trying to be practical enough to write real programs, sharp enough to cut your desk in half, and unfinished enough that you should not point it at payroll software unless payroll has personally wronged you.
The compiler frontend is written in Go. It tokenizes, parses, gathers scope information, checks links and types, monomorphizes generics, emits LLVM IR, then lets Clang turn that IR into something your operating system can be blamed for.
For the full language tour, read docs/SYNTAX.md. This README is the version for people that can't read too good.
mod main
use "../std/allocator.mg" alc
use "../std/heap.mg" heap
use "../std/io.mg" io
main(args str[]) !void:
a alc.Allocator = heap.allocator()
stdout := try io.stdout(a)
defer stdout.close()
out := stdout.writer()
try out.writeLn("Hello, World!")
..
Yes, main can take args str[]. Yes, it can throw. No, this does not make it
enterprise-grade. Please stop asking.
Magma currently has:
- modules declared with
mod - imports with
use "path" alias - functions, throwing functions, and
void - structs and methods with implicit
this - generic structs, generic functions, and generic receiver methods
- local variables with explicit types or
:=inference - zero-initialized locals and globals
- assignments to names, fields, nested fields, and indexed values
- pointers, slices, fixed-size arrays,
sizeof,addrof,&,*, and indexing - conditionals with
if,elif, andelse while,break, andcontinuedeferstatements anddefer:blocks- a first-class
errortype withtry,throw, and result destructuring - external function declarations with
ext - inline LLVM strings with
llvm @platform(...)directives for platform-specific imports/declarations- ownership-transfer annotations, destructor declarations, and a warning-only destroy/borrow checker
- a standard library covering allocation, heap, files, buffered IO, readers, writers, strings, slices, memory, UTF-8 helpers, lists, queues, maps, casts, and errors
Here is a less tiny example, because apparently some people will judge a language beyond just "hello world":
mod main
use "../std/allocator.mg" alc
use "../std/errors.mg" errors
use "../std/heap.mg" heap
use "../std/io.mg" io
use "../std/list.mg" list
Pair[A, B](
left A,
right B,
)
swap[A, B](p Pair[A, B]) Pair[B, A]:
out Pair[B, A]
out.left = p.right
out.right = p.left
ret out
..
mightThrow(fail bool) !u8:
if fail:
throw errors.failure("asked to fail")
..
ret 42
..
main(args str[]) !void:
a := heap.allocator()
stdout := try io.stdout(a)
defer stdout.close()
out := stdout.writer()
value u8, err error = mightThrow(false)
if errors.code(err) != 0:
throw err
..
xs := try list.new[u8](a)
try xs.pushRight(a, value)
got u8 = try xs.popLeft(a)
if got != 42:
throw errors.failure("math has abandoned us")
..
try out.writeLn("Passed the tiny trial.")
..
Throwing return types use !:
readByte() !u8:
ret 42
..
try unwraps a throwing call or returns the error from the current throwing
function:
byte u8 = try readByte()
throw conditionally returns an error. Throwing errors.ok() is a no-op,
which is weirdly elegant if you squint:
throw errors.invalidArgument("bad argument")
throw errors.ok()
You can also destructure a throwing call when you want to handle the error yourself:
value u8, err error = readByte()
if errors.code(err) != 0:
throw err
..
Blocks start with : and end with ... Indentation is for humans, and humans
are on probation.
while i < n:
if shouldStop:
break
..
i = i + 1
..
The same block shape is used for functions, methods, conditionals, loops, and
multi-line defer.
$T marks an ownership-transfer position. An owned return gives the result to
the caller, while an owned parameter consumes its argument. The same T
without $ is borrowed:
Resource(data ptr)
destr Resource.free() void:
# release this.data
..
make() $Resource:
value Resource
ret value
..
consume(value $Resource) void:
value.free()
..
The destr modifier marks a struct member as a destructor. Destructors may have
arguments and may throw, but their result type must be void/!void. They are
not called automatically: call one explicitly or register it with defer.
The compiler's destroy checker follows owned destructible locals across direct assignments, calls, returns, branches, loops, and scope exits. It warns about leaks, consuming borrowed values, double consumption, use after transfer, overwrites, and discarded owned results. Warnings do not stop compilation, and the analysis intentionally does not model fields, indexed storage, pointers, aliases, or partial moves. See Ownership and Destruction.
The std/ directory is already doing useful low-level work:
allocator,heap: allocation interfaces and platform heap implementationsio,file,reader,writer,buffered: file and stream IOerrors: error constructors, codes, messages, comparison helpersstrings,slices,utf8: string/slice utilities and UTF-8/UTF-16 helpersmemory,cast: memory operations and explicit castsarray,list,queue,linear_map,hash_map,builder: containers and buildershttp: Windows streaming HTTP through WinHTTPraylib: initial Windows raylib 5.5 window, drawing, and input bindings
Platform-specific pieces live under std/win/ and std/unix/, selected with
@platform(...).
The interesting bits live here:
main.go: command-line entry pointsrc/tokenizer: tokenizationsrc/parser: AST constructionsrc/scope_info: scope mapssrc/checker: link and type checkingsrc/monomorph: generic monomorphizationsrc/llvm_ir: LLVM IR loweringsrc/ir_cleaner: cleanup pass for emitted IRstd/: Magma standard librarysamples/: sample programs and smoke testsdocs/SYNTAX.md: syntax reference and current edge casesdocs/OWNERSHIP.md: ownership, borrowing, destructors, and checker limits
The compiler accepts one input .mg file and emits LLVM IR by default.
go build
.\Magma.exe samples\minimal.mg
clang.exe out.ll -o out.exe
.\out.exeCompiler options:
--debug print compiler diagnostics
--version, -v print the compiler version
--out, -o <path> choose the output path
--emit, -e <llvm|object|exe>
--opt, -O <0|1|2|3> choose the LLVM optimization level
--clang-path print the resolved Clang executable path
--clang-version, -cv print the resolved Clang version and path
Object, executable, and optimized LLVM output require Clang. Magma searches
MAGMA_CLANG first, followed by PATH, LLVM_HOME/LLVM_PATH, standard LLVM
install locations, Visual Studio's LLVM directories on Windows, and common
LLVM locations on Unix and macOS.
You need:
- Go matching the module requirement in go.mod, currently
1.24.6 - Clang/LLVM, because Magma emits LLVM IR and Clang does the final lowering
On Windows, the batch scripts are the paved road:
BUILD&RUN_TESTS.bat: build the compiler, lowersamples/tests.mg, compileout.ll, and run the resultBUILD&LOWER_SAMPLE.bat: build and lowersamples/impl.mg, also emitting optimized LLVM snapshotsFULL_COMP_O1&RUN_SAMPLE.bat: build, lowersamples/minimal.mg, compile with-O1, and run itFULL_COMP_O3&RUN_SAMPLE.bat: same idea, with-O3COMP&RUN.bat: compile an existingout.lland runout.exe
If clang.exe is not on PATH, either put it there or edit the scripts. This
is not a Magma feature. This is your machine expressing itself.
Unix support exists in the standard library via std/unix/, but the checked-in
automation is Windows batch files. Bring your own shell commands and emotional
stability.
Magma is still a compiler project, not a lifestyle brand. The useful caveats:
forloops are planned; usewhilefor now.- Global variables are zero-initialized; top-level initializers are still WIP.
- Implicit struct constructors are planned, not something to rely on today.
- Ownership checking is warning-only and limited to direct local variables; raw pointers, aliases, fields, indexed values, and partial moves remain unchecked.
- The checker is incomplete. Some invalid return values, casts, throwing calls, and pointer adventures may get farther than they deserve.
- There are no bounds checks, null checks, or read-only string mutation guards.
- Inline LLVM is accepted as text. If it is nonsense, LLVM will have opinions.
See TODO.md for the current pile of sharp edges.
Magma is deliberately direct: explicit blocks, explicit allocation, explicit errors, explicit casts, explicit enough that the compiler sometimes hands you the screwdriver and walks away.
The goal is not to become the language equivalent of a padded meeting room. The goal is a small systems language with readable syntax, first-class errors, generics, a usable standard library, and enough low-level escape hatches to make bad decisions efficiently.
That may sound irresponsible, but at least it is honest.
Read docs/SYNTAX.md, run BUILD&RUN_TESTS.bat, and remember: if the
generated program segfaults, that still counts as native performance.