Language reference

Syntax, types, expressions, declarations, modules, and packages.

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 + second

Line 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-3

Boolean 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:

TypeMeaning
intDefault signed 64-bit integer
i8, i16, i32, i64Explicit-width signed integers
u8, u16, u32, u64Explicit-width unsigned integers
floatDefault 64-bit floating-point number
f3232-bit storage at numerical and C boundaries
booltrue or false
stringImmutable UTF-8 bytes
voidNo meaningful value
ErrorNamed error value with a message and optional details

Common composite types:

FormMeaning
[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) -> RFunction value
Future<T>Asynchronous work producing T
Iterator<T>Lazy single-pass sequence
NdArray<T>Dense shaped numerical array
*TPointer 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) -> bool

moon does not implicitly mix integers and floats. Use as when a conversion should be explicit.

Operators

KindOperators
Arithmetic+, -, *, /, %
Comparison==, !=, <, <=, >, >=
Logical`&&`, ``, `!`
Bitwise`&`, ``, `^`, `<<`, `>>`, `>>>`
Assignment=, +=, -=, *=, /=, %=, and bitwise compound forms
Otheras, .., 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 u8

Float-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? = none

Use 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) = position

A 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 += 5

Patterns destructure values while binding names:

let (x, y) = point
let Point { x, y } = value

Top-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_HEIGHT

A 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"] += 1

Compound 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) // 1

Arrays and dictionaries are references. Another binding observes mutations to the same collection:

let values = [1]
let alias = values
alias.push(2)
println(values.len()) // 2

Pointers 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.mn
use 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.greet

Use 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.

SpellingMeaning
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.