Getting Started

Write your first Loon program in 15 minutes — and see the compiler catch a leaked password before it ships.

1. Install the compiler

Loon compiles on Linux x86-64. You need NASM (the Netwide Assembler) and a linker — both come standard on most distributions.

Linux (x86-64)

Install NASM, clone the repository, and build the bootstrap chain:

# Install the assembler
sudo apt install nasm

# Clone the repository
git clone https://github.com/mplsllc/loon.git
cd loon

# Build the bootstrap chain: stage0 (lexer) then stage1 (compiler)
cd stage0 && make && cd ../stage1 && make && cd ..

# Compile the stage2 compiler (written in Loon) using the bootstrap chain
./stage0/lexer stage2/compiler.loon | ./stage1/compiler > /tmp/loon.asm
nasm -f elf64 -o /tmp/loon.o /tmp/loon.asm
ld -o loon /tmp/loon.o

# Install system-wide
sudo mv loon /usr/local/bin/

macOS / Windows

Loon targets Linux x86-64 natively. For macOS and Windows, see the Building from Source documentation for cross-compilation options and WSL instructions.

2. Hello world

Create a file called hello.loon:

module main;

fn main() [IO] -> Unit {
    do print("Hello, Loon!");
    do exit(0);
}

Compile and run it:

loon hello.loon > hello.asm && nasm -f elf64 -o hello.o hello.asm && ld -o hello hello.o && ./hello

You should see Hello, Loon! printed to the terminal.

Three things to notice:

3. Functions and effects

Define a pure function — one that takes values and returns a value, with no side effects:

fn add(a: Int, b: Int) [] -> Int {
    a + b
}

The empty brackets [] mean this function has no effects. It cannot print, it cannot read files, it cannot do anything other than compute a value from its inputs.

What happens if a pure function tries to call print?

fn add(a: Int, b: Int) [] -> Int {
    do print("adding");  // try to sneak in some I/O
    a + b
}
error[E0301]: effect violation in function `add`
  --> math.loon:2:5
  |
2 |     do print("adding");
  |     ^^^^^^^^^^^^^^^^^^^ `print` requires [IO] effect
  |
  = note: function `add` declares effects [] but calls require [IO]
  = help: add IO to the effect list: fn add(a: Int, b: Int) [IO] -> Int

The compiler rejects it. A function that claims to be pure must actually be pure. There is no way to sneak a side effect into a pure function.

4. Types and ADTs

Loon has algebraic data types (ADTs). Define a Shape type with three variants:

type Shape =
  | Circle(Float)
  | Rectangle(Float, Float)
  | Point;

Use match to handle each variant:

fn describe(s: Shape) [] -> String {
    match s {
        Circle(r) -> "Circle with radius " + float_to_string(r),
        Rectangle(w, h) -> "Rectangle " + float_to_string(w) + "x" + float_to_string(h),
        Point -> "Point",
    }
}

Every match in Loon must be exhaustive. If you forget a variant, the compiler tells you:

fn describe(s: Shape) [] -> String {
    match s {
        Circle(r) -> "Circle with radius " + float_to_string(r),
        Rectangle(w, h) -> "Rectangle",
        // Point is missing
    }
}
error[E0401]: non-exhaustive match expression
  --> shapes.loon:8:5
  |
8 |     match s {
  |     ^^^^^^^^ missing variant: `Point`
  |
  = note: all variants of `Shape` must be handled
  = help: add the missing arm: Point -> ...

No variant falls through. No default case silently swallows a new variant added six months later. If the type changes, every match that handles it must be updated, and the compiler will tell you exactly which ones.

5. Privacy types

This is the moment. This is why Loon exists.

Create a file called auth.loon:

module auth;

fn main() [IO] -> Unit {
    let username: Public<String> = "alice";
    let password: Sensitive<String> = "hunter2";

    do print("User: " + username);     // OK
    // do print("Pass: " + password);  // COMPILE ERROR

    do exit(0);
}

Uncomment the second print line. The compiler rejects it:

error[E0501]: privacy violation — Sensitive value cannot flow to public output
  --> auth.loon:8:27
  |
8 |     do print("Pass: " + password);
  |                          ^^^^^^^^ `password` has privacy level Sensitive
  |
  = note: `print` is a public output channel
  = note: Sensitive data cannot be printed, logged, serialized, or returned from a public API
  = help: use expose(password, "reason") with [Audit] effect to explicitly declassify

It is not just print. Try assigning the password to a plain variable:

let copy: String = password;  // COMPILE ERROR
error[E0502]: privacy violation — cannot assign Sensitive<String> to String
  --> auth.loon:9:24
  |
9 |     let copy: String = password;
  |                         ^^^^^^^^ Sensitive<String> cannot be assigned to String
  |
  = note: this would strip the Sensitive wrapper, allowing the value to leak

Try passing it to a function that accepts a plain String:

fn log_value(v: String) [IO] -> Unit {
    do print(v);
}

// In main:
do log_value(password);  // COMPILE ERROR
error[E0503]: privacy violation — Sensitive<String> cannot flow to String parameter
  --> auth.loon:14:19
   |
14 |     do log_value(password);
   |                  ^^^^^^^^ expected String, found Sensitive<String>
   |
   = note: function `log_value` accepts String, which is a public type
   = note: passing Sensitive data to a public parameter would allow it to leak

The compiler blocks every path. You cannot print it, assign it to an unwrapped variable, pass it to a function that does not expect sensitive data, or serialize it. The password is locked at the type level.

6. The escape hatch: expose()

Sometimes you genuinely need the underlying value — to hash it, compare it, or send it over an encrypted channel. Loon provides expose() for this, but it requires the Audit effect and a reason string:

fn hash_password(pw: Sensitive<String>) [Audit] -> String {
    let raw: String = expose(pw, "hashing for storage");
    hash_sha256(raw)
}

This compiles. Three things happen:

7. ZeroOnDrop

For cryptographic keys, API secrets, and other values that should not linger in memory after use, Loon provides the ZeroOnDrop policy:

let api_key: Sensitive<String, ZeroOnDrop> = load_key();
// use api_key...
// when api_key goes out of scope, its memory is zeroed

ZeroOnDrop guarantees that the memory backing this value is overwritten with zeros when the value is no longer reachable. This is not a finalizer that might run eventually — it is a deterministic cleanup tied to scope exit. The value cannot survive in memory after the function returns.

Combined with privacy types, this means a sensitive value is protected at every stage: it cannot leak through code at compile time, and it cannot leak through memory at runtime.

What's next

You have seen the core of Loon: effects that make side effects visible, types that make data shapes exhaustive, and privacy types that make leaks impossible. Here is where to go from here: