Language Reference

Every construct in Loon, concisely defined. Not a tutorial — a reference.

1. Modules

Every source file begins with a module declaration. One module per file. No exceptions.

module my_module;

The module name is an identifier. It must appear before any other declaration.

2. Functions

Functions are declared with fn. Parameters are typed. Effects are declared in square brackets. The return type follows ->. The last expression in the body is the return value — there is no return keyword.

fn name(param: Type, param2: Type) [Effects] -> ReturnType {
    body
}
Parameters
Each parameter requires a name and a type annotation, separated by :. Multiple parameters are comma-separated.
Effect declaration
Square brackets list the effects a function may perform. Use [] for a pure function with no effects. Multiple effects are comma-separated: [IO, FileIO].
Return type
Declared after ->. Every function must declare its return type.
Return value
The last expression in the function body is the return value. There is no return keyword.

Example:

fn greet(name: String) [IO] -> Unit {
    do print(string_concat("Hello, ", name))
}

fn double(n: Int) [] -> Int {
    n * 2
}

3. Variables

Variables are declared with let. All variables are immutable. Type annotation is required.

let name: Type = value;
Immutability
All variables are immutable by default. There is no mut keyword. Reassignment is a compile error.
Type annotation
Required on every let binding. The compiler does not infer variable types.
Scope
Variables are scoped to the enclosing block.

Example:

let count: Int = 42;
let message: String = "hello";

4. Types

Primitive types

TypeDescription
Int64-bit signed integer
StringImmutable UTF-8 string
Booltrue or false
UnitNo meaningful value (similar to void)

Privacy-wrapped types

TypeDescription
Public<T>Data safe for public consumption
Sensitive<T>Data requiring protection (passwords, PII)
Restricted<T>Data with access restrictions

Privacy types accept an optional policy parameter:

Sensitive<String, ZeroOnDrop>

User-defined algebraic data types

Declared with type. Each variant may carry named fields or be a bare tag.

type Shape {
    Circle(radius: Int),
    Rectangle(width: Int, height: Int),
    Point,
}

Variants are comma-separated. Fields within a variant use the same name: Type syntax as function parameters.

5. Match Expressions

Pattern matching over algebraic data types. Match is an expression — it produces a value.

match value {
    Pattern1 -> expression1,
    Pattern2(a, b) -> expression2,
}
Exhaustiveness
Every variant of the matched type must be covered. The compiler rejects non-exhaustive matches.
Arms
Each arm is Pattern -> expression. Arms are comma-separated.
Destructuring
Variant fields are bound to names in the pattern: Circle(r) binds the radius to r.
Expression result
Every arm must produce a value of the same type.

Example:

let description: String = match shape {
    Circle(r) -> string_concat("circle with radius ", int_to_string(r)),
    Rectangle(w, h) -> string_concat("rectangle ", int_to_string(w)),
    Point -> "a point",
};

6. For Loops

Iteration over ranges.

for i in range(0, 10) {
    // body
}
range(start, end)
Produces integers from start (inclusive) to end (exclusive).
Loop variable
Immutable within the loop body. Scoped to the loop.

7. Effects and do

Effects declare what a function may do beyond pure computation. A function that performs I/O must declare [IO]. A function that reads files must declare [FileIO]. A pure function declares [].

Built-in effects

EffectPermits
IOConsole output, process exit
FileIOFile reads, command-line argument access
CryptoCryptographic operations (hashing)
AuditPrivacy boundary crossing via expose()

The do keyword

Calling an effectful function requires the do keyword at the call site. This makes effects visible in the code, not just the signature.

fn main() [IO] -> Unit {
    do print("hello")
}

Calling print without do is a compile error. Declaring effects you don't use is allowed. Performing effects you didn't declare is not.

8. Privacy Types

Loon enforces data classification at compile time through a privacy type hierarchy.

Hierarchy

Public < Restricted < Sensitive

No implicit downcast
A Sensitive<String> cannot be passed where a Public<String> is expected. The compiler rejects it.
Crossing boundaries
The expose() function crosses privacy boundaries. It requires the Audit effect, creating a traceable record of every declassification.
Policies
Privacy types accept an optional policy: Sensitive<String, ZeroOnDrop>. Policies control runtime behavior like memory clearing.
fn log_user(password: Sensitive<String>) [IO, Audit] -> Unit {
    let safe: Public<String> = do expose(password, "logging");
    do print(safe)
}

9. Built-in Functions

FunctionSignatureDescription
print (s: String) [IO] -> Unit Print string to stdout
exit (code: Int) [IO] -> Unit Exit the process with a status code
int_to_string (n: Int) [] -> String Convert integer to its string representation
string_concat (a: String, b: String) [] -> String Concatenate two strings (also available via +)
read_file (path: String) [FileIO] -> String Read entire file contents as a string
get_arg (n: Int) [FileIO] -> String Get command-line argument at index n
hash_password (pw: Sensitive<String>) [Crypto] -> Hashed<String> Hash a sensitive password
expose (val: Sensitive<T>, reason: String) [Audit] -> Public<T> Cross a privacy boundary with an audit reason
array (size: Int) [] -> Array<T> Create an array of the given size
len (arr: Array<T>) [] -> Int Return the length of an array

10. Operators

CategoryOperatorsNotes
Arithmetic + - * / % Integer arithmetic. Division truncates toward zero.
Comparison == != < > <= >= Produce Bool. Both operands must have the same type.
Logical and or not Boolean logic. Keywords, not symbols.
String concatenation + When both operands are String, + concatenates.
Pipe |> Passes the left-hand value as the first argument to the right-hand function.

There is no operator overloading. Each operator has exactly one meaning per type.

11. Compile-time Checks

The compiler enforces these checks before emitting any code. Programs that violate any check do not compile.

  1. Effect violations — Calling an effectful function without declaring its effect in the caller's signature.
  2. Type mismatches — Passing a value of the wrong type to a function or binding.
  3. Undefined variables — Referencing a variable that has not been declared in the current scope.
  4. Wrong argument count — Calling a function with too many or too few arguments.
  5. Immutable reassignment — Assigning to a variable that has already been bound.
  6. Return type mismatch — The last expression in a function body does not match the declared return type.
  7. Non-exhaustive match — A match expression that does not cover every variant of the matched type.
  8. Undefined functions — Calling a function that has not been declared.
  9. Effect requirements for read_file and get_arg — These functions require [FileIO]; calling them without it is rejected.

12. Structured Errors

The compiler emits errors as JSON objects for machine consumption. Every error includes four fields:

code
A stable error identifier (e.g., "E001").
message
A human-readable description of the error.
location
File, line, and column where the error was detected.
suggestion
An optional fix the developer or AI agent can apply.

Example:

{
  "code": "E003",
  "message": "effect IO required but not declared",
  "location": {"line": 5, "col": 3},
  "suggestion": "add IO to the function's effect list: [IO]"
}

Structured errors make Loon compiler output directly consumable by AI agents, IDEs, and CI pipelines without parsing free-form text.