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
returnkeyword.
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
mutkeyword. Reassignment is a compile error. - Type annotation
- Required on every
letbinding. 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
| Type | Description |
|---|---|
Int | 64-bit signed integer |
String | Immutable UTF-8 string |
Bool | true or false |
Unit | No meaningful value (similar to void) |
Privacy-wrapped types
| Type | Description |
|---|---|
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 tor. - 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) toend(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
| Effect | Permits |
|---|---|
IO | Console output, process exit |
FileIO | File reads, command-line argument access |
Crypto | Cryptographic operations (hashing) |
Audit | Privacy 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 aPublic<String>is expected. The compiler rejects it. - Crossing boundaries
- The
expose()function crosses privacy boundaries. It requires theAuditeffect, 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
| Function | Signature | Description |
|---|---|---|
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
| Category | Operators | Notes |
|---|---|---|
| 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.
- Effect violations — Calling an effectful function without declaring its effect in the caller's signature.
- Type mismatches — Passing a value of the wrong type to a function or binding.
- Undefined variables — Referencing a variable that has not been declared in the current scope.
- Wrong argument count — Calling a function with too many or too few arguments.
- Immutable reassignment — Assigning to a variable that has already been bound.
- Return type mismatch — The last expression in a function body does not match the declared return type.
- Non-exhaustive match — A match expression that does not cover every variant of the matched type.
- Undefined functions — Calling a function that has not been declared.
- Effect requirements for
read_fileandget_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.