This reference describes the language as a moon programmer sees it: the values you can create, the expressions you can write, and how modules and packages fit together. For a guided introduction, start with the interactive tour.
Example
struct Person {
name: string,
age: int,
}
fn greeting(person: Person) -> string {
let category = if person.age < 18 { "young" } else { "grown" }
"Hello ${person.name}, you are $category."
}
fn main() {
let people = [
Person { name: "Ada", age: 36 },
Person { name: "moon", age: 1 },
]
for person in people {
println(greeting(person))
}
}moon is statically typed and expression-oriented. Blocks, if, and match can produce values, while type inference keeps local code concise.
Source Text
moon files are UTF-8 text files with the .mn extension. Whitespace separates tokens but indentation does not change meaning. Semicolons are optional.
let first = 1
let second = 2; let total = first + secondLine comments start with //. Block comments use /* ... */ and may be nested. A run of /// comments immediately above a declaration becomes its documentation:
/// Returns the larger value.
pub fn larger(left: int, right: int) -> int {
if left > right { left } else { right }
}Identifiers start with a letter or underscore and continue with letters, digits, or underscores. moon code conventionally uses snake_case for values and functions and PascalCase for types.
Literals
Integers may be decimal, hexadecimal, binary, or octal. Underscores can group digits. Floats may use a decimal exponent.
let mask = 0xFF
let bits = 0b1010
let mode = 0o755
let million = 1_000_000
let small = 1.5e-3Boolean literals are true and false. Strings use double quotes and support \\, \", \n, \t, \r, and \$ escapes.
String interpolation accepts $name or ${expression}:
let name = "moon"
let release = 1
println("Hello $name ${release + 1}")Arrays, dictionaries, tuples, structs, and enums have their own literal or constructor syntax in the sections below.
Types
Primitive types:
| Type | Meaning |
|---|---|
int | Default signed 64-bit integer |
i8, i16, i32, i64 | Explicit-width signed integers |
u8, u16, u32, u64 | Explicit-width unsigned integers |
float | Default 64-bit floating-point number |
f32 | 32-bit storage at numerical and C boundaries |
bool | true or false |
string | Immutable UTF-8 bytes |
void | No meaningful value |
Error | Named error value with a message and optional details |
Common composite types:
| Form | Meaning |
|---|---|
[T] | Growable array of T |
{K: V} | Dictionary from K to V |
(A, B) | Tuple |
Optional<T> or T? | Present or absent value |
Result<T, E> | Success or failure |
fn(A, B) -> R | Function value |
Future<T> | Asynchronous work producing T |
Iterator<T> | Lazy single-pass sequence |
NdArray<T> | Dense shaped numerical array |
*T | Pointer used for addressable moon values or C memory |
Named structs, enums, interfaces, and aliases may take generic arguments:
type Pair<T> = (T, T)
type Predicate = fn(int) -> boolmoon does not implicitly mix integers and floats. Use as when a conversion should be explicit.
Operators
| Kind | Operators | ||
|---|---|---|---|
| Arithmetic | +, -, *, /, % | ||
| Comparison | ==, !=, <, <=, >, >= | ||
| Logical | `&&`, ` | `, `!` | |
| Bitwise | `&`, ` | `, `^`, `<<`, `>>`, `>>>` | |
| Assignment | =, +=, -=, *=, /=, %=, and bitwise compound forms | ||
| Other | as, .., postfix ?, prefix await, &place, *pointer |
&& and || short-circuit. Integer division truncates toward zero. + also concatenates strings. Parentheses override precedence.
From low to high, the main precedence groups are ||, &&, equality, bitwise operators, ordering and shifts, addition, multiplication, casts, prefix operators, then calls and member access.
Expressions
Most moon constructs produce a value. An expression used only for its effect may stand as a statement.
Blocks
A block contains statements followed by an optional final expression. That final expression is the block's value:
let answer = {
let base = 40
let extra = 2
base + extra
}Without a final expression, a block has type void. {} is an empty block; {:} is an empty dictionary.
If Expressions
An if condition must be bool. When if is used as a value, it needs an else and both branches must have the same type:
let label = if score >= 60 { "pass" } else { "retry" }For effect-only code, else is optional. if let runs a branch when a pattern matches and makes the pattern's bindings available in that branch:
if let some(value) = lookup(name) {
println(value)
} else {
println("missing")
}Match Expressions
match tries arms from top to bottom and selects the first matching pattern:
let label = match number {
0 => "zero",
1 | 2 => "small",
value if value < 0 => "negative",
_ => "many",
}A value-producing match requires all arms to have the same type. Matches over booleans, optionals, results, enums, and their combinations must cover every case. Open-ended values such as integers and strings usually need a wildcard arm.
Calls, Paths, Fields, Methods, and Indexing
add(1, 2)
std.math.sqrt(16.0)
Color.Red
Shape.Circle(2.0)
point.x
tuple.0
array[0]
dictionary["key"]
text.trim_ascii()Calls and arguments evaluate from left to right. A method call such as text.trim_ascii() uses the same public function signature as its qualified form.
Generic functions usually infer their type arguments. Supply them explicitly when needed:
let value = first<int>([1, 2, 3])Casts
as performs an explicit scalar or pointer conversion:
let count: int = 5
let ratio: float = count as float
let byte: u8 = count as u8Float-to-integer casts truncate toward zero. Converting between fixed integer widths may narrow or wrap, so keep the cast close to the boundary that requires it.
Ranges
start..end creates a half-open integer range: it includes start and excludes end.
for index in 0..3 {
println(index)
}Range patterns may use ..= for an inclusive upper bound.
Optionals
Optional<T> represents a value that may be present. T? is a shorthand.
let found: Optional<int> = some(42)
let missing: int? = noneUse match, if let, is_some, is_none, unwrap, or unwrap_or to inspect one. Postfix ? unwraps some(value) or returns none from the enclosing optional-returning function:
fn first_doubled(values: [int]) -> Optional<int> {
let value = values.first_or_none()?
some(value * 2)
}Results and Error Propagation
Result<T, E> represents success or failure:
fn divide(left: int, right: int) -> Result<int, Error> {
if right == 0 {
err.DivisionByZero("cannot divide by zero")
} else {
ok(left / right)
}
}Match ok(value) and err(error) when both cases need local handling. Error provides name, message, code, and details.
Postfix ? unwraps success or returns the error from the enclosing function:
fn compute(a: int, b: int, c: int) -> Result<int, Error> {
let first = divide(a, b)?
let second = divide(first, c)?
ok(second)
}Use errors for expected application failures. unwrap() is most appropriate when failure would indicate a programming mistake or in a small example.
Function Values
Named functions and non-variadic functions can be stored and passed as values:
fn double(value: int) -> int { value * 2 }
fn apply(operation: fn(int) -> int, value: int) -> int {
operation(value)
}
let operation = double
println(apply(operation, 4))A function value has an exact positional signature. Named arguments and declaration defaults apply only when calling a known declaration directly.
Closures
A closure writes |parameters| expression or returns a block:
let multiplier = 3
let scale = |value: int| value * multiplier
println(scale(4))Closures capture surrounding bindings by reference. If a captured binding is mutable, changes inside and outside the closure see the same value.
Arrays
[T] is a growable ordered collection:
let mut values = [1, 2, 3]
values.push(4)
values[0] = 10
println(values.len())Array elements have one type. Common methods include map, filter, flat_map, sorted_by, group_by, chunked, first_or_none, and explicit mutating forms such as push, remove_at, and sort_in_place.
Indexing outside the array is an error. Use get_or_none or element_at_or_none when absence is part of normal control flow.
N-Dimensional Arrays
NdArray<T> stores dense numeric or boolean data with a shape:
use std.ndarray
let matrix = ndarray.from_array<f32>(
[1.0, 2.0, 3.0, 4.0],
[2, 2],
)?
println(matrix.shape())
println(matrix.transpose2()?.to_array())It supports checked indexing, views, broadcasting, reductions, dot products, and matrix multiplication. See the numerical computing guide for the normal workflow.
Dictionaries
A dictionary maps keys to values and preserves insertion order:
let mut ages = {"Ada": 36, "Grace": 36}
ages["Ada"] += 1
for (name, age) in ages {
println("$name: $age")
}{:} creates an empty dictionary when its type is known. Keys may be integers, strings, booleans, payload-free enums, or tuples of those types. Common methods include get, set, has, remove, keys, and values.
Comprehensions and Generator Expressions
Comprehensions combine an output expression with for clauses and optional guards:
let squares = [value * value for value in 0..6 if value % 2 == 0]
let parity = {value % 2: value for value in 0..6}Square brackets build an array; braces build a dictionary. Parentheses create a lazy generator:
let first_three = (value * value for value in 0..1000).take(3)Multiple for clauses nest from left to right. Generator expressions do not pull their input until an iterator consumer asks for a value.
Tuples
Tuples group positional values, which may have different types:
let position = ("west", 3)
println(position.0)
let (direction, steps) = positionA one-element tuple needs a trailing comma: (value,).
Structs
Structs are named values with fields:
pub struct Point {
x: int,
y: int,
}
let point = Point { x: 10, y: 20 }
let moved = Point { ...point, x: point.x + 1 }All fields are required unless a spread supplies them. Field order in the literal does not matter. Field assignment requires a mutable root binding.
Bytes (Growable Byte Buffer)
std.bytes provides a growable byte buffer for incremental binary or text construction:
use std.bytes
let buffer = bytes.new()
buffer.append_string("moon")
buffer.push(10)
let output = buffer.to_string()Use append, append_string, append_int, and append_json_escaped to build content, then to_string or to_array at the boundary that needs it.
Enums
Enums define a closed set of variants. Variants may carry values:
enum Shape {
Circle(float),
Rectangle(float, float),
Empty,
}
let shape = Shape.Circle(5.0)When the expected enum type is known, constructors may use .Variant:
let shape: Shape = .Rectangle(10.0, 20.0)Enums may be generic. Use match to inspect a value and extract variant payloads.
Patterns
Patterns destructure values in let, if let, while let, for, and match:
let (name, age) = record
let Point { x, y } = point
match event {
Event.Key(name) => println(name),
Event.Resize(width, height) if width > height => println("wide"),
_ => println("other"),
}Useful forms include _, binding names, literals, integer ranges, tuples, structs, enum variants, some, none, ok, err, and array forms such as [head, ..tail]. | combines alternatives in a match arm.
An ordinary let pattern must always match. For a refutable pattern, add an else block that leaves the current scope:
let [head, ..tail] = values else {
return err.Empty("expected at least one value")
}Interfaces and Impl Blocks
An interface describes methods a type provides:
interface Greet {
fn name(self: string) -> string
fn greeting(self: string) -> string {
"Hello, ${self.name()}!"
}
}Implement it for a struct:
struct Person { value: string }
impl Greet for Person {
fn name(self: Person) -> string { self.value }
}An impl Type block attaches inherent methods without an interface:
impl Point {
fn move(self: Point, dx: int, dy: int) -> Point {
Point { x: self.x + dx, y: self.y + dy }
}
}Generic functions may require interface bounds:
fn welcome<T: Greet>(value: T) -> string {
value.greeting()
}Interfaces may declare associated types with type Item; an implementation binds one with type Item = ConcreteType.
Async and Futures
An async fn returns Future<T>. await waits for it inside another async function:
async fn answer() -> int {
await sleep(10)
42
}
async fn main() {
println(await answer())
}spawn starts a future, join and join_all wait for several, select reports the first to finish, and cancel requests cancellation. then maps a successful future value; catch handles an unexpected task failure.
See Concurrency for practical patterns and the distinction between async tasks and multi-core work.
Channels
std.channel passes typed values between async tasks:
let channel = std.channel.new<int>(8)
await channel.send(42)
let value = await channel.recv()
channel.close()send waits when a bounded channel is full. recv waits when it is empty and returns none after it is closed and drained. Capacity 0 creates a rendezvous channel.
Isolates
An isolate runs a named top-level function independently on another core:
fn square(value: int) -> int { value * value }
fn main() {
let job = std.isolate.spawn("square", [6])
println(std.isolate.join(job))
}Plain arguments and results cross by copy. Closures, futures, and pointers do not cross. Use std.parallel when applying one named function across a collection is the desired shape.
Pointers, Externs, and Native Interop
&value lends read-only access to a Moon value; &mut value lends exclusive access to a mutable value. Parameter types use &T or &mut T. Views are checked against storage lifetimes and do not keep a local alive after its scope ends.
Imported foreign values use their C names directly. Raw addresses use c.Pointer<T> or c.ReadOnlyPointer<T>, with nullable pointers checked before use. Raw pointer access and raw foreign calls require unsafe, which does not establish ownership or lifetime.
Foreign layout comes from the imported C declaration. Handwritten @C and @packed attributes, source header/link directives, and callback or memory annotations are retired. Ordinary Moon structs do not assert C ABI compatibility.
The current source runner does not yet discover native modules or libraries. External signatures can be checked as declarations, but executable calls require native bindings. Queued and retained callback source APIs remain unavailable. The Calling C guide describes these boundaries.
Statements and Declarations
Let Bindings
Local bindings are immutable by default. Add mut when the binding or a field reached through it will be assigned:
let name = "moon"
let count: int = 1
let mut score = 10
score += 5Patterns destructure values while binding names:
let (x, y) = point
let Point { x, y } = valueTop-Level Constants
A top-level let or const defines a compile-time constant. Add pub to export it:
pub let WINDOW_WIDTH: int = 1280
const WINDOW_HEIGHT = 720
let AREA = WINDOW_WIDTH * WINDOW_HEIGHTA top-level let mut defines private mutable module state. Export functions that read or update it instead of exposing the cell itself:
let mut count = 0
pub fn next_count() -> int {
count += 1
count
}Assignment
Assignment targets a mutable local, field, tuple slot, index, or pointer:
let mut point = Point { x: 0, y: 0 }
point.x = 3
let mut counts = {"a": 1}
counts["a"] += 1Compound assignment evaluates the target once. Assignment is a statement and does not produce a value.
Value and Reference Semantics
Structs and tuples are values. Binding, passing, or storing one copies it:
let original = Point { x: 1, y: 2 }
let mut copy = original
copy.x = 9
println(original.x) // 1Arrays and dictionaries are references. Another binding observes mutations to the same collection:
let values = [1]
let alias = values
alias.push(2)
println(values.len()) // 2Pointers preserve the identity of their target. Closures share captured bindings by reference.
Functions
Parameters always have types. Omit the return type for a void function:
pub fn add(left: int, right: int) -> int {
left + right
}
fn log(message: string) {
println(message)
}A non-void function returns its final expression or uses return. The program entry point is fn main() or async fn main().
Generic functions declare type parameters after the name:
fn identity<T>(value: T) -> T { value }const fn declares a non-generic function that may be called while computing a top-level constant.
Default Parameter Values
Trailing parameters may provide defaults:
fn endpoint(host: string, port: int = 8080, secure: bool = false) -> string {
"$host:$port:$secure"
}
endpoint("localhost")
endpoint("localhost", 9000)Once one parameter has a default, every following non-variadic parameter needs one. A default expression runs each time an argument is omitted.
Named Arguments
Name an argument to skip defaults or make a call clearer:
endpoint("example.com", secure: true)
endpoint(port: 9000, host: "localhost")Positional arguments come before named ones. Each parameter may be supplied once, and arguments still evaluate in the order they are written.
Variadic Functions
A final ...T parameter collects extra arguments into [T]:
fn sum(values: ...int) -> int {
let mut total = 0
for value in values { total += value }
total
}
sum()
sum(1, 2, 3)There is no spread-call syntax. When forwarding an existing array, make the array-taking function the main implementation and use a small variadic wrapper for direct callers.
Loops
while repeats while a boolean condition is true. while let repeats while a pattern matches:
while let some(line) = lines.next() {
println(line)
}for iterates arrays, ranges, dictionaries, and iterators:
for value in values {
println(value)
}
for (key, value) in dictionary {
println("$key: $value")
}break exits a loop and continue starts its next iteration. Label a loop to target an outer one:
search: for row in rows {
for cell in row {
if cell == target { break search }
}
}Iterators
Iterator<T> is a lazy, single-pass cursor. next() returns some(value) or none. Two bindings to one iterator share its position; calling .iter() separately creates a new cursor.
let numbers = (0..100)
.iter()
.filter(|value| value % 2 == 0)
.map(|value| value * value)
println(numbers.take(3).collect())Adapters such as map, filter, flat_map, take, skip, enumerate, zip, and chunked are lazy. Terminals such as collect, fold, count, find, any, and for_each consume the remaining values.
Defer
defer schedules cleanup for the enclosing block. Several defers in one block run in reverse order:
fn read_header(path: string) -> Result<string, Error> {
let file = std.fs.try_open(path, "r")?
defer file.close()
ok(file.read(64))
}Cleanup runs on normal block exit, return, ?, break, and continue. In a loop body it runs once per iteration. A deferred action cannot itself return, propagate with ?, or await.
Modules
Each .mn file is a module. Directories form dotted namespaces:
project/
main.mn
math.mn
shapes/
area.mnuse math
use math.add
use shapes.area
use std.strings.{len, to_upper as upper}
use std.math.*Items are private by default. Add pub to make one importable. Qualified access uses ., as in math.add(1, 2) or shapes.area.circle(radius).
pub use re-exports a public import from the current module.
Packages
A project declares its name, version, and dependencies in moon.toml:
[package]
name = "demo-app"
version = "0.1.0"
[[deps]]
name = "greeter"
git = "https://github.com/example/greeter.git"
version = "^1.2"The package name becomes the first part of its import path:
use greeter.hello.greetUse moon add, moon update, moon deps, and moon vendor to manage dependencies. See Tooling for the workflow.
Memory
moon has no garbage collector. Memory is reclaimed by scoped arenas and by owners, both visible in the source.
context.allocator.use(.Arena) selects a fresh arena for the rest of the enclosing block. Everything allocated while it is selected — including allocations made by the functions the block calls, and across await — comes from that arena, and the arena is released when the block exits, on every exit path:
fn shout(names: [string]) -> string {
context.allocator.use(.Arena)
let loud = names.map(|name: string| name.to_upper())
loud.join(" ")
}A value that escapes the block — the result, a returned value, a store into something that outlives the block — is copied into the older memory first.
| Spelling | Meaning |
|---|---|
context.allocator.use(.Arena) | Select a fresh arena for the rest of the block |
context.allocator.use(other) | Select an allocator you already hold |
allocator.Arena.scoped() | A named arena, released when its block exits |
allocator.Arena.new() | An arena released by deinit(), and by nothing else |
arena.bytes() | What an arena currently holds |
Buffer, Array<T>, and Box<T> are owner types: each holds memory freed exactly once, by deinit(). Ownership is part of the type, so no annotation is needed. Binding, appending, storing in a field, or returning an owner moves it; view(), get(i), and &items[i] borrow it; clone() copies it. Pair the construction with defer for the release, and use errdefer for a step that can fail before ownership moves on:
fn collect(texts: [string]) -> Result<Array<Buffer>, Error> {
let mut words = Array.new<Buffer>()
errdefer words.deinit()
for text in texts {
let mut word = Buffer.new()
errdefer word.deinit()
word.append(text)?
words.append(word)?
}
ok(words)
}Open files, child processes, and C handles are owners in the same sense. moon check reports a use after release, an exit that leaves an owner unreleased, a release through something that does not own the value, and an owner built in an arena but stored where it would outlive one.
See Memory for the guide, including long-running loops and arenas that outlive their block.
Built-ins
Common built-ins include print, println, to_string, sleep, spawn, join, join_all, select, and cancel. Collection behavior is available through typed methods on arrays, dictionaries, iterators, optionals, results, futures, and ndarrays.
Use the API reference for exact signatures and the standard library modules that build on these types.
Formal Grammar
The core declaration shapes are:
let [mut] pattern [: Type] = expression
fn name<TypeParams>(parameters) [-> Type] block
async fn name(parameters) [-> Type] block
struct Name<TypeParams> { fields }
enum Name<TypeParams> { variants }
interface Name { methods and associated types }
impl [Interface for] Type { methods and associated type bindings }
use dotted.path[.{items}]Expressions include literals, names and dotted paths, calls, field and index access, unary and binary operators, casts, blocks, if, match, closures, collection literals, comprehensions, ranges, await, and postfix ?.
Statements include let, assignment, return, defer, while, for, break, continue, and an expression used for its effect. Newlines or semicolons separate adjacent statements.
For a concrete syntax question, the surrounding sections are usually easier to use than a production grammar. moon fmt shows the canonical formatting, and moon check reports incomplete or ambiguous syntax at the source location.