Start here · First steps
Hello, moon
A moon program starts in main. Function bodies are blocks, and the last expression in a block is its value. This first function returns void, so its only work is calling the built-in println function.
Edit the text and press Run. The runner compiles this exact .mn file with the toolchain version shown beside the result. moon source files always use the .mn extension.
fn main() {
println("Hello moon")
}
Open editable lesson →
Start here · First steps
Read a type error
Add : Type after a binding to state its type. Here the annotation says count is an int, but the value is a string. Check reports the problem without running the program and points back to main.mn.
Change "three" to 3. The automatic check is debounced, so it waits for a pause in your typing before it compiles.
fn main() {
let count: int = "three"
println(count)
}
Open editable lesson →
Start here · First steps
Use another module
Every .mn file is a module. use greeting loads greeting.mn, and greeting.message selects its public function. Declarations are private until you add pub.
Switch between the two file tabs, edit either file, then run them as one request. The two files you see are the whole program. The entry file anchors module paths, so sibling modules use their file name.
use greeting
fn main() {
println(greeting.message("Ada"))
}
Open editable lesson →
Values and expressions · Values
Source text and comments
moon source is UTF-8 and whitespace is not significant. A semicolon may separate statements on one line. // starts a line comment; /* ... */ comments may nest. A run of /// immediately before a declaration becomes its documentation.
Identifiers start with a letter or underscore, followed by letters, digits, or underscores. Comments may appear between tokens, so removing one never joins its neighbours.
/// Returns a small greeting.
fn greeting() -> string {
/* outer /* nested */ comment */
"Hello"
}
fn main() {
let _name2 = greeting(); println(_name2) // line comment
}
Open editable lesson →
Values and expressions · Values
Numbers and booleans
Integers are decimal by default and may use 0x, 0b, or 0o prefixes. Underscores group digits without changing the value. A decimal point or exponent makes a float. Booleans are true and false.
Plain integer and float literals infer int and float; a surrounding type can select a narrower numeric representation.
fn main() {
println(0xFF | 0b1010)
println(0o755)
println(1_000)
println(1.5e2 > 100.0)
}
Open editable lesson →
Values and expressions · Values
Strings and interpolation
Strings use double quotes and are immutable UTF-8 values. Familiar escapes include \n, \t, \r, \\, and \". Interpolate one name with $name or any expression with ${...}.
Use \$ when a dollar sign is literal. Numeric and boolean interpolation converts the value to text; it does not change the value's type elsewhere.
fn main() {
let name = "moon"
let age = 1
println("Hello $name")
println("Next: ${age + 1}")
println("Price: \$5")
}
Open editable lesson →
Values and expressions · Values
Bindings and inference
let introduces an immutable local binding and always needs an initializer. Usually moon infers the type from that value. Write : Type when the annotation explains intent or resolves otherwise ambiguous syntax.
An immutable binding cannot be reassigned, though it may refer to a mutable reference value such as an array.
fn main() {
let language: string = "moon"
let letters = 4
println("$language has $letters letters")
}
Open editable lesson →
Values and expressions · Values
Mutation and assignment
Add mut when the binding itself must change. Assignment is a statement and produces no value. Compound forms such as +=, *=, and <<= read and write the target once.
moon does not convert the assigned value implicitly, so the right side must already have the target's type.
fn main() {
let mut score = 5
score += 1
score *= 4
println(score)
}
Open editable lesson →
Values and expressions · Values
Blocks are expressions
A block evaluates its final expression. A trailing statement, or no expression at all, gives the block the void value. This lets a calculation keep temporary bindings close to the result.
Braces containing key: value entries are a dictionary. Empty {} is a block; write {:} for an empty dictionary.
fn main() {
let answer = {
let base = 40
let extra = 2
base + extra
}
println(answer)
}
Open editable lesson →
Values and expressions · Values
Numeric types and casts
Use int and float for ordinary arithmetic. Width-specific forms such as u8, i32, and f32 are useful for file formats and native boundaries; bool, string, and void cover the other basic values.
moon does not mix numeric types implicitly. as Type requests an explicit scalar conversion. Arithmetic on a narrow integer keeps that width, so overflow wraps as the example shows. Later lessons introduce collection, function, optional, result, and pointer types where they are used.
fn main() {
let near: u8 = 250 as u8
let step: u8 = 10 as u8
let wrapped: u8 = near + step
println(wrapped)
}
Open editable lesson →
Values and expressions · Values
Operators and precedence
Arithmetic binds more tightly than comparison, comparison more tightly than &&, and && more tightly than ||. Parentheses make a different grouping explicit. Logical operators short-circuit.
Integer values also support &, |, ^, and three shifts. >> preserves a sign bit; >>> shifts in zeros. A negative or oversized shift yields zero.
fn main() {
println(2 + 3 * 4)
println(10 > 3 && !false)
println((1 | 2) << 2)
}
Open editable lesson →
Functions · Functions and calls
Declare a function
Functions live at module scope or inside an impl. Every parameter has a type. Write -> Type for a meaningful return value; a function without it returns void.
The final expression is returned automatically. Add pub only when another module must import the function. main is the parameterless entry point and is not overloaded.
fn add(left: int, right: int) -> int {
left + right
}
fn main() {
println(add(3, 4))
}
Open editable lesson →
Functions · Functions and calls
Return early
A tail expression handles the ordinary path. return value leaves the function immediately, which is useful for a small guard. A void function may use bare return.
Every path in a value-returning function must produce the declared type. moon checks that before any path runs.
fn admission(age: int) -> string {
if age < 18 { return "minor" }
"free"
}
fn main() {
println(admission(20))
println(admission(12))
}
Open editable lesson →
Functions · Functions and calls
Default arguments
A trailing parameter may declare = expression. A caller can omit it, and every later non-variadic parameter must also have a default. The expression runs at each omitted call in module scope.
Defaults belong to direct declarations. A call through a stored function still requires its exact positional signature.
fn endpoint(host: string, port: int = 8080) -> string {
"$host:$port"
}
fn main() {
println(endpoint("local"))
println(endpoint("local", 9000))
}
Open editable lesson →
Functions · Functions and calls
Name call arguments
name: expression binds an argument to that parameter. Positional arguments come first; named arguments may then appear in any order and can skip a default.
Expressions still evaluate from left to right as written. Named calls apply only when moon knows the declaration, so function values and built-in calls take exact positional arguments.
fn endpoint(host: string, port: int = 8080, secure: bool = false) -> string {
"$host:$port:$secure"
}
fn main() {
println(endpoint("example", secure: true))
println(endpoint(secure: false, host: "other", port: 9000))
}
Open editable lesson →
Functions · Functions and calls
Collect variadic arguments
Mark the final parameter name: ...Type to collect surplus arguments into a fresh array. A call may supply none. The fixed part of the signature still has normal arity.
Variadic functions are called directly. They cannot be stored as function values, passed by name, or fed an existing array with spread syntax.
fn sum(values: ...int) -> int {
let mut total = 0
for value in values { total += value }
total
}
fn main() {
println(sum())
println(sum(1, 2, 3, 4))
}
Open editable lesson →
Functions · Functions and calls
Pass function values
A named, fixed-signature function is a value. Its type spells parameter types and a return type: fn(int) -> int. Store it, return it, or pass it to a higher-order function.
Indirect calls use exact positional arguments. Defaults and names are properties of a known declaration, and the function value's type does not carry them.
fn double(value: int) -> int { value * 2 }
fn apply(f: fn(int) -> int, value: int) -> int {
f(value)
}
fn main() {
let operation = double
println(apply(operation, 4))
}
Open editable lesson →
Functions · Functions and calls
Capture with closures
A closure writes |parameters| expression or returns a block. Types may be inferred from context. Captured bindings are shared by reference, so mutations inside and outside observe the same value.
A let-bound closure may call itself. Give recursive closures an explicit return type when the base case cannot make it obvious.
fn main() {
let mut count = 4
let bump = || -> int { count += 1; count }
println(bump())
println(bump())
println(count)
}
Open editable lesson →
Functions · Functions and calls
Write a generic function
Place type parameters after a function's name. moon normally infers them from arguments and context. A direct call can write function<Type>(...) when an empty collection or another argument supplies no clue.
The bare angle-bracket spelling is moon's explicit call form. It takes no dot or colon prefix.
fn identity<T>(value: T) -> T { value }
fn first<T>(values: [T]) -> Optional<T> {
if values.len() == 0 { none } else { some(values[0]) }
}
fn main() {
println(identity("moon"))
println(first<int>([7]))
}
Open editable lesson →
Functions · Functions and calls
Evaluate a function at compile time
An ordinary function can compute a constant. moon evaluates the call while building the program when every argument is a compile-time constant.
No marker is needed, but the body must be pure and bounded. moon reports a check error at the constant when the function performs I/O, creates a shared object or does not terminate.
fn square(value: int) -> int { value * value }
let SIDE = square(12)
fn main() {
println(SIDE)
}
Open editable lesson →
Collections and iteration · Collections
Arrays
[T] is a dynamic array whose elements share one type. Indexing is bounds-checked. Methods cover mutation, safe selection, mapping, filtering, grouping, ordering, and aggregation.
Arrays are reference values, so another binding aliases the same collection. A structural change such as push is visible through both names. Non-mutating helpers return fresh arrays.
fn main() {
let values = [1, 2, 3]
let alias = values
alias.push(4)
println(values)
println(values.map(|x| x * 2))
println(values[3])
}
Open editable lesson →
Collections and iteration · Collections
Dictionaries
{K: V} maps keys to values. Keys may be integers, strings, booleans, payload-free enums, or tuples made from those types. {:} is the empty dictionary.
A bare loop binding receives keys. A top-level tuple pattern receives (key, value) entries. Dictionaries preserve first-insertion order, including when a value is replaced.
fn main() {
let mut ages = {"Ada": 36, "Grace": 36}
ages["Ada"] += 1
for (name, age) in ages {
println("$name:$age")
}
}
Open editable lesson →
Collections and iteration · Collections
Tuples
A tuple groups a fixed number of values, possibly of different types. Select a slot with .0, .1, and so on, or destructure the whole value in a pattern.
Tuples are value types, so binding or passing one copies its slots. A one-element tuple needs a trailing comma: (value,).
fn main() {
let position = ("west", 3)
let (direction, steps) = position
println(direction)
println(steps)
}
Open editable lesson →
Collections and iteration · Collections
Integer ranges
start..end creates a half-open integer range that includes start and stops before end. A descending range walks downward. A range has start and end fields.
Inclusive ..= belongs in patterns, where it matches both bounds. Keeping iteration half-open makes lengths and indexes compose directly.
fn main() {
for index in 0..3 {
println(index)
}
}
Open editable lesson →
Collections and iteration · Collections
Eager comprehensions
A comprehension puts the produced value first, followed by one or more for clauses and optional if guards. Square brackets build an array; braces build a dictionary.
Clauses nest from left to right. Later equal dictionary keys replace the value without moving the key's original position.
fn main() {
let squares = [x * x for x in 0..6 if x % 2 == 0]
let parity = {x % 2: x for x in 0..5}
println(squares)
println(parity)
}
Open editable lesson →
Collections and iteration · Collections
Lazy generator expressions
Parenthesized comprehension syntax creates an Iterator<T> instead of an eager collection. Its sources, guards, and produced value run only as downstream pulls demand them.
Generators are synchronous, fused, and single-pass. A terminal such as collect makes the eager boundary visible; take can stop before entering unused work.
fn main() {
let even_squares = (x * x for x in 0..1000 if x % 2 == 0)
println(even_squares.take(3).collect())
}
Open editable lesson →
Collections and iteration · Collections
Iterator cursors
Iterator<T> is a fused, single-pass cursor. next() advances it and returns Optional<T>. Copying the iterator aliases that cursor; calling iter() again on a collection creates an independent one.
Adapters such as map, filter, and take stay lazy. Terminal methods consume from the current position.
fn main() {
let source = [1, 2, 3]
let first = source.iter()
let alias = first
let second = source.iter()
println(first.next())
println(alias.next())
println(second.next())
}
Open editable lesson →
Collections and iteration · Collections
N-dimensional arrays
NdArray<T> stores numeric or boolean elements compactly with a runtime shape and strides. Constructors validate dimensions and return Result; views such as slice, transpose, and broadcast can share storage.
Elementwise arithmetic broadcasts compatible shapes. Content comparison is explicit because ordinary equality tests ndarray identity.
use std.ndarray
fn matrix_size() -> Result<int, Error> {
let matrix = ndarray.from_array<f32>([1.0, 2.0, 3.0, 4.0], [2, 2])?
ok(matrix.len())
}
fn main() {}
Open editable lesson →
Collections and iteration · Collections
Build with Bytes
Bytes is a growable byte buffer and moon's efficient string builder. Appending avoids the repeated whole-string copies caused by a long chain of + operations.
Byte values are checked to 0..=255. to_string() preserves the accumulated bytes; slice copies rather than exposing an alias.
use std.bytes
fn main() {
let buffer = bytes.new()
buffer.append_string("moon")
buffer.push(32)
buffer.append_string("1")
println(buffer.to_string())
}
Open editable lesson →
Control flow and patterns · Control flow
If expressions
An if condition is always bool. For effects, else is optional. When the expression supplies a value, every branch must exist and produce the same type.
Because blocks have values, you do not need a separate conditional operator. Put the chosen if directly on the right side of a binding.
fn main() {
let count = 12
let size = if count > 10 { "large" } else { "small" }
println(size)
}
Open editable lesson →
Control flow and patterns · Control flow
Test one pattern with if let
if let pattern = expression evaluates the initializer once, then runs the true branch only when the refutable pattern matches. New bindings exist only in that branch.
Use it for a single success path. A full match is clearer when several cases carry useful information.
fn main() {
let answer: Optional<int> = some(42)
if let some(value) = answer {
println(value)
} else {
println("missing")
}
}
Open editable lesson →
Control flow and patterns · Control flow
While and while let
while condition repeats while a boolean remains true. while let reevaluates an expression and repeats while its refutable pattern matches, creating fresh bindings each time.
continue starts the next test; break exits the nearest loop. The pattern form is a natural way to drain Iterator.next().
fn main() {
let mut index = 0
while index < 2 {
println(index)
index += 1
}
let cursor = [2, 3, 4].iter()
while let some(value) = cursor.next() {
println(value)
}
}
Open editable lesson →
Control flow and patterns · Control flow
For loops
for pattern in iterable consumes an array, range, dictionary, or iterator. The loop head must match every item, so it uses an irrefutable pattern.
Bindings are immutable, scoped to the body, and fresh each iteration. A tuple head destructures tuple elements, including values produced by enumerate().
fn main() {
for (index, letter) in ["a", "b"].enumerate() {
println("$index:$letter")
}
}
Open editable lesson →
Control flow and patterns · Control flow
Name a loop
Place name: before a loop to give it a label. break name exits that loop through any nested loops; continue name advances or retests it.
Labels are local to a function and visible only inside their loop body. They occupy a separate namespace from values and types.
fn main() {
search: for row in [[1, 2], [3, 4]] {
for cell in row {
if cell == 3 {
println("found $cell")
break search
}
}
}
println("done")
}
Open editable lesson →
Control flow and patterns · Control flow
Match exhaustively
match tries arms in order and selects the first matching pattern. Used as a value, every arm produces one type.
moon rejects missing cases and unreachable arms. Open types such as integers and strings need a wildcard or binding to close the match; finite enums and optionals can be proven complete case by case.
fn describe(value: int) -> string {
match value {
0 => "zero",
1 | 2 => "small",
_ => "many",
}
}
fn main() { println(describe(2)) }
Open editable lesson →
Control flow and patterns · Control flow
Literal and range patterns
Patterns can compare exact integer, boolean, or string literals. Integer ranges use half-open .. or inclusive ..= bounds. Strings can match a prefix, suffix, or contained part.
These are refutable patterns, so they belong in a match, conditional pattern, or a let with a divergent failure branch.
fn age_group(age: int) -> string {
match age { 13..=19 => "teen", _ => "other" }
}
fn kind(path: string) -> string {
match path { ..".zip" => "archive", _ => "other" }
}
fn main() {
println(age_group(16))
println(kind("notes.zip"))
}
Open editable lesson →
Control flow and patterns · Control flow
Structural patterns
Structural patterns mirror the value's shape. Tuple patterns name fixed slots. Array patterns may require fixed positions and capture a remainder with ..name.
whole @ pattern binds the complete matched value while also refining its inside. Nested patterns compose, provided every successful alternative binds the same names and types.
fn main() {
let point = (0, 0)
println(match point { (0, 0) => "origin", _ => "elsewhere" })
let values = [1, 2, 3]
match values {
[head, ..tail] => println("head $head, tail $tail"),
_ => println("empty"),
}
}
Open editable lesson →
Control flow and patterns · Control flow
Guards and or-patterns
Separate top-level alternatives with |. Every alternative must bind the same names with the same types. Add if condition after a pattern to require a guard.
Guards run only after the pattern matches. They do not make a match exhaustive, so keep an unguarded case for every remaining value.
fn classify(value: int) -> string {
match value {
x if x < 0 => "negative",
0 | 1 | 2 => "small",
_ => "large",
}
}
fn main() {
println(classify(-1))
println(classify(2))
}
Open editable lesson →
Control flow and patterns · Control flow
Publish a refutable binding
An ordinary let pattern must always match. A refutable pattern adds else whose block leaves the surrounding scope through return, break, or continue.
That divergence proves the successful bindings exist afterward. The initializer runs once, and failure cannot use names that were never bound.
fn first(values: [int]) -> string {
let [head, .._] = values else { return "empty" }
"first $head"
}
fn main() {
println(first([5, 6]))
println(first([]))
}
Open editable lesson →
Control flow and patterns · Control flow
Optional values
Optional<T> is either some(value) or none; T? is equivalent type shorthand. none needs context from an annotation, branch, argument, or return type.
Match both cases for explicit control. Inside a function returning another optional, postfix ? unwraps some or returns none early.
fn label(name: Optional<string>) -> string {
match name {
some(value) => value,
none => "unknown",
}
}
fn main() {
println(label(some("Ada")))
println(label(none))
}
Open editable lesson →
Control flow and patterns · Control flow
Results and propagation
Result<T, E> is ok(value) or err(error). The built-in err.Name("message") constructs an Error with stable name and human context.
Postfix ? unwraps success or returns the same error type from the enclosing result function. Match err(error) when you need its fields.
fn divide(a: int, b: int) -> Result<int, Error> {
if b == 0 { err.DivisionByZero("zero divisor") } else { ok(a / b) }
}
fn main() {
println(divide(8, 2))
match divide(8, 0) {
ok(value) => println(value),
err(error) => println(error.name),
}
}
Open editable lesson →
Control flow and patterns · Control flow
See a missing match case
Finite types let moon prove exactly which cases are missing. This match handles some but omits none, so Check names the uncovered case.
Add none => 0. A catch-all also closes the match, but explicit finite cases preserve the checker's help when the type later grows.
fn number(value: Optional<int>) -> int {
match value {
some(inner) => inner,
}
}
fn main() {}
Open editable lesson →
Control flow and patterns · Control flow
Defer block cleanup
defer statement schedules work when the enclosing block exits. Defers are block-scoped and run in reverse registration order on fall-through, return, propagation, break, or continue.
They read current values at exit time. Runtime errors abort without running them, and a defer body cannot redirect control with return, propagation, or await.
fn main() {
defer println("first")
defer println("second")
println("work")
}
Open editable lesson →
User-defined types · Structs, enums, and interfaces
Structs
A struct is a named group of typed fields. A literal provides every field in any order. Spread update starts from an existing value, then replaces the named fields that follow.
Field assignment requires a mutable root. Struct patterns mirror the declaration and may name, refine, or ignore fields.
struct Point {
x: int,
y: int,
}
fn main() {
let origin = Point { x: 0, y: 2 }
let moved = Point { ...origin, x: 3 }
println("${moved.x},${moved.y}")
}
Open editable lesson →
User-defined types · Structs, enums, and interfaces
Value and reference semantics
Structs and tuples are values, so binding, assignment, arguments, and collection insertion all copy them. Arrays and dictionaries are references, so another binding observes mutation of the same collection.
Pointers copy target identity. Closures share captured bindings by reference. None of these rules involve hidden copy-on-write behavior.
struct Box { value: int }
fn main() {
let original = Box { value: 1 }
let mut copy = original
copy.value = 9
println(original.value)
let items = [1]
let alias = items
alias.push(2)
println(items.len())
}
Open editable lesson →
User-defined types · Structs, enums, and interfaces
Enums with payloads
An enum is a tagged union. A unit variant carries only its identity; a payload variant carries typed positional values. Construct with Enum.Variant(...) and inspect with a pattern.
moon checks enum matches for exhaustiveness, including payload structure, and rejects arms that can never be reached.
enum Shape {
Dot,
Circle(float),
}
fn describe(shape: Shape) -> string {
match shape {
Shape.Dot => "dot",
Shape.Circle(radius) => "circle $radius",
}
}
fn main() { println(describe(Shape.Circle(2.5))) }
Open editable lesson →
User-defined types · Structs, enums, and interfaces
Contextual enum variants
When an expected enum type is known, .Variant is enough for unit or payload construction. A function's return type gives its tail expression that context, just as a type annotation does for a binding. The same shorthand works in patterns, calls, imports, and aliases.
Without expected context, moon accepts the shorthand only when one visible enum owns that variant; otherwise it reports every candidate.
enum Signal { Go, Stop }
fn parse_label(signal: string) -> Signal {
if signal == "go" { .Go } else { .Stop }
}
fn label(signal: Signal) -> string {
match signal { .Go => "go", .Stop => "stop" }
}
fn main() {
let signal: Signal = .Stop
println(label(parse_label("go")))
println(label(signal))
}
Open editable lesson →
User-defined types · Structs, enums, and interfaces
Generic structs and enums
Structs and enums may declare type parameters after their names. Each concrete argument produces a checked instantiation that keeps the nominal identity of the declaration.
Expected arguments also guide contextual variants and empty collection payloads, so Box<int> keeps int even when a variant carries no value.
enum Box<T> {
Value(T),
Empty,
}
fn read(box: Box<int>) -> Optional<int> {
match box { .Value(value) => some(value), .Empty => none }
}
fn main() { println(read(.Value(7))) }
Open editable lesson →
User-defined types · Structs, enums, and interfaces
Type aliases
type Name = ExistingType gives another spelling to the same type. An alias is transparent, so it adds vocabulary without creating a new nominal identity or a conversion boundary.
An alias may take type parameters and may name any complete type expression, including tuples, functions, optionals, and instantiated user types.
type Pair = (int, int)
type Maybe<T> = Optional<T>
fn show(pair: Pair) -> string { "${pair.0},${pair.1}" }
fn main() {
let value: Maybe<Pair> = some((3, 4))
println(show(value.unwrap()))
}
Open editable lesson →
User-defined types · Structs, enums, and interfaces
Inherent methods
impl Type attaches inherent methods. A method's first parameter is self: Type; call it with receiver syntax and supply only the remaining arguments.
Methods may be public, generic, or async. Receiver selection also works through a managed pointer without exposing a raw address.
struct Point { x: int, y: int }
impl Point {
fn move(self: Point, dx: int, dy: int) -> Point {
Point { x: self.x + dx, y: self.y + dy }
}
}
fn main() {
let point = Point { x: 1, y: 2 }.move(3, 4)
println("${point.x},${point.y}")
}
Open editable lesson →
User-defined types · Structs, enums, and interfaces
Interfaces and impls
An interface declares method contracts. impl Interface for Type supplies them, after which the concrete value can be passed where the interface type is expected.
Calls through the interface use the implementation supplied for the value's type. Each impl must match the interface's parameter and return types exactly.
interface Describe {
fn describe(self: string) -> string
}
struct Dog { name: string }
impl Describe for Dog {
fn describe(self: Dog) -> string { "Dog: ${self.name}" }
}
fn print_item(item: Describe) { println(item.describe()) }
fn main() { print_item(Dog { name: "Rex" }) }
Open editable lesson →
User-defined types · Structs, enums, and interfaces
Default methods and bounds
An interface method may include a default body. An impl inherits it unless it provides an override. Defaults may call other methods from the same contract.
T: Interface constrains a generic parameter and makes those methods available in the body. Join several required interfaces with +.
interface Greet {
fn name(self: string) -> string
fn greeting(self: string) -> string { "Hello, ${self.name()}!" }
}
struct Person { value: string }
impl Greet for Person {
fn name(self: Person) -> string { self.value }
}
fn welcome<T: Greet>(value: T) -> string { value.greeting() }
fn main() { println(welcome(Person { value: "Ada" })) }
Open editable lesson →
User-defined types · Structs, enums, and interfaces
Associated interface types
An interface may name an associated type with type Name. Each impl binds it with type Name = Concrete, which keeps the related types inside one contract.
Associated types describe a capability's output without adding a free type argument at every use. The binding belongs to the impl and is checked with its methods.
interface Source {
type Item
fn count(self: string) -> int
}
struct Numbers { values: [int] }
impl Source for Numbers {
type Item = int
fn count(self: Numbers) -> int { self.values.len() }
}
fn main() {}
Open editable lesson →
User-defined types · Structs, enums, and interfaces
Overload struct operators
Reserved inherent method names define operators for a struct. For example, binary + calls add(self, other), while unary - calls neg(self).
Comparison overloads and not return bool. Compound assignment uses the same overloaded binary operation before storing the result.
struct Point { x: int, y: int }
impl Point {
fn add(self: Point, other: Point) -> Point {
Point { x: self.x + other.x, y: self.y + other.y }
}
}
fn main() {
let point = Point { x: 1, y: 2 } + Point { x: 4, y: 5 }
println("${point.x},${point.y}")
}
Open editable lesson →
User-defined types · Structs, enums, and interfaces
Call a function field
A struct field whose substituted type is a function can be called through the receiver. This gives callback records the same readable call shape as methods.
An actual method with the same name takes precedence. Function-field calls use exact positional arguments, like every indirect call.
struct Strategy<T> {
apply: fn(T) -> T,
}
fn main() {
let strategy: Strategy<int> = Strategy { apply: |value: int| value * 3 }
println(strategy.apply(4))
}
Open editable lesson →
Modules and projects · Modules, packages, and binaries
Module paths
Each .mn file is a module. Directories between it and the entry file become dotted namespace segments: shapes/area.mn is shapes.area.
Paths are anchored at the entry directory from every file. There is no relative-import spelling, and a directory alone is not a module unless a matching file exists.
use shapes.area
fn main() {
println(area.rectangle(3, 4))
}
Open editable lesson →
Modules and projects · Modules, packages, and binaries
Import modules and items
use toolkit imports a module alias. use toolkit.add imports one item; braces group items and as renames a leaf. use toolkit.* imports names declared by that module.
Qualified access always uses dots. A wildcard does not recursively re-export or flatten names that its module merely imported.
use toolkit
use toolkit.{add as sum, multiply}
use extras.*
fn main() {
println(toolkit.add(3, 4))
println(multiply(3, 4))
println(sum(4, five()))
}
Open editable lesson →
Modules and projects · Modules, packages, and binaries
Re-export a public surface
Prefix an item import with pub to re-export it. A facade can therefore present a stable public surface while its implementation remains split across modules.
Only public declarations can cross this boundary. Re-exporting does not copy an item; callers retain its original types, defaults, and declaring-module identity.
use api.{greet}
fn main() { println(greet("Ada")) }
Open editable lesson →
Modules and projects · Modules, packages, and binaries
Keep module details private
Declarations are private to their module until marked pub. This keeps helper names out of the import surface while allowing public functions to use them internally.
Check rejects this import at the boundary. Add pub to secret, or export a public operation that owns the private detail.
use vault
fn main() { println(vault.secret()) }
Open editable lesson →
Modules and projects · Modules, packages, and binaries
Module-owned type identity
A nominal type's declaring module is part of its identity. Two modules may both export Flag; their values remain distinct even if the variants look identical.
Use a module prefix on types, constructors, and patterns to state which declaration you mean. Bare lookup prefers the declaration in the current module.
use cli
use web
fn main() {
println(cli.show(cli.Flag.On))
println(web.show(web.Flag.On))
}
Open editable lesson →
Modules and projects · Modules, packages, and binaries
Constants and module state
Top-level let and const create compile-time constants and may be public. Top-level let mut creates private mutable module state for the running program.
Modules initialize in dependency order and top-to-bottom. Export functions around mutable state instead of exporting the cell. The concurrency chapter explains what happens when work moves to an isolate.
let NAME = "moon"
let mut count = 0
fn next() -> string {
count += 1
"$NAME $count"
}
fn main() {
println(next())
println(next())
}
Open editable lesson →
Modules and projects · Modules, packages, and binaries
Package manifests
moon.toml gives a project a package name and dependency records. A dependency's name becomes the first segment of its import namespace. Sources may be local, Git, bundled, or registry-backed.
moon deps validates the transitive graph and writes checksummed moon.lock; moon vendor creates an offline copy. Package resolution stays local-only in this tour sandbox.
use greeter.hello
fn main() { println(hello.message("moon")) }
Open editable lesson →
Modules and projects · Modules, packages, and binaries
Multiple project binaries
[[bin]] maps an entry file to a portable executable name. [bins] folder opts into sorted, non-recursive discovery of direct .mn children; an explicit record overrides the discovered name.
Build and check may select several binaries. Run, source docs, and debug select exactly one. Aggregate commands share dependency resolution while keeping entry failures separate.
use shared
fn main() { println(shared.label("worker")) }
Open editable lesson →
Memory · Arenas, owners, and resources
What happens by default
Ordinary code looks like implicit, with no mention of memory anywhere. Every value comes from the allocator the running task has selected, here the process heap, so no signature carries an allocator and no call site passes one.
explicit is the same four lines with that written down. allocator.Arena.new() makes an allocator you hold, use points those lines at it, and arena.deinit() gives the memory back. The output is identical; the second version says where the bytes came from and when they were released.
moon has no garbage collector, and implicit still has nothing to release. Nothing owns greeting or words, and an unowned value in a block that selected no arena lives until the process exits, which is fine for four lines and a problem in a loop. The rest of the chapter deals with that.
fn implicit() {
let greeting = "Hello " + "moon"
let words = greeting.split(" ")
println(words.len())
println(words[0])
}
fn explicit() {
let arena = allocator.Arena.new()
context.allocator.use(arena)
let greeting = "Hello " + "moon"
let words = greeting.split(" ")
println(words.len())
println(words[0])
arena.deinit()
}
fn main() {
implicit()
explicit()
}
Open editable lesson →
Memory · Arenas, owners, and resources
Give a block its own memory
The manual form of the same thing is to say which allocator a block uses. context.allocator.use(.Arena) selects a fresh arena for the rest of the enclosing block. Everything allocated while it is selected, including inside the functions the block calls, is released together when the block ends.
A loop body is the usual place for one. Each iteration gets its own arena, so the strings and arrays it builds are reclaimed at the closing brace instead of piling up for the life of the program.
fn main() {
let mut total = 0
for line in ["a,b", "c,d", "e,f"] {
context.allocator.use(.Arena)
let parts = line.split(",")
total += parts.len()
}
println(total)
}
Open editable lesson →
Memory · Arenas, owners, and resources
Values that leave the block
A value that leaves the block (the block's own result, a returned value, or a store into something that outlives it) is copied out before the arena is released, so the caller holds a copy.
So a function can stage its intermediate work in an arena and still return a result. The scratch strings map produced here go away with the block; the joined string the caller prints does not.
fn shout(names: [string]) -> string {
context.allocator.use(.Arena)
let loud = names.map(|name: string| name.to_upper())
loud.join(" ")
}
fn main() {
println(shout(["ada", "moon"]))
}
Open editable lesson →
Memory · Arenas, owners, and resources
Owners free what they hold
Buffer, Array<T>, and Box<T> are owner types: each one holds memory that is freed exactly once, by deinit(). Ownership is part of the type, so there is nothing to annotate.
Pair the construction with a defer and the release happens on every exit from the block, including return, ?, break, and continue. view() hands out a read-only look at the bytes; it borrows, so it stays valid only while the owner does.
fn main() {
let mut text = Buffer.new()
defer text.deinit()
text.append("Hello ").unwrap()
text.append("moon").unwrap()
println(text.view())
}
Open editable lesson →
Memory · Arenas, owners, and resources
A container owns its elements
An Array<T> of owners owns its elements too. append moves the element in, and releasing the array releases everything it holds. You never free an element separately.
Between building an element and handing it over, the element is still yours. errdefer covers that gap. It releases the value if the step in between fails, and does nothing on the successful path where ownership moves on. The compiler asks for that guard when a failure would otherwise strand the value.
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)
}
fn main() {
let mut words = collect(["moon", "shine"]).unwrap()
defer words.deinit()
println(words.len())
println(words.get(0).view())
}
Open editable lesson →
Memory · Arenas, owners, and resources
Using a value after it is freed
Nothing about ownership waits until the program runs. Check follows each owner through the body and reports a read that happens after the release, naming the value, where it was created, and the line that freed it.
The first read here is fine and the second is not. Move it above text.deinit(), or release with defer text.deinit() instead, so the buffer lives to the end of the block.
fn main() {
let mut text = Buffer.new()
println(text.len())
text.deinit()
println(text.len())
}
Open editable lesson →
Memory · Arenas, owners, and resources
Storing an arena value somewhere older
Ordinary values are copied out of an arena when they escape, but an owner is not. It holds memory the arena is about to release, and a copy would give it a second release. Storing one into a container that outlives the block is reported, naming both values.
The error suggests two fixes: build the buffer outside the block, or select the array's allocator with context.allocator.use(..) before building the buffer, so both live in the same place.
fn main() {
let mut rows = Array.new<Buffer>()
defer rows.deinit()
{
context.allocator.use(.Arena)
let mut row = Buffer.new()
errdefer row.deinit()
row.append("moon").unwrap()
rows.append(row).unwrap()
}
println(rows.len())
}
Open editable lesson →
Memory · Arenas, owners, and resources
Files and processes are owners too
An open file, a child process and its pipes, and a handle from a C library carry a cleanup obligation in the same way a Buffer does. The binding that acquires one owns it, passing it by value hands the obligation on, and the release happens once.
Check reports a block that is left without releasing, and it reports a release through a copy that does not own the resource. defer on the line after the acquisition always satisfies both.
fn main() {
let notes = std.fs.try_open("notes.txt", "w").unwrap()
defer notes.close()
notes.write("moon\n")
println("saved")
}
Open editable lesson →
Advanced concurrency · Async work, channels, and isolates
Async functions and await
Calling an async fn creates a lazy Future<T>. await starts it if needed, suspends the current task, and produces its T; only async functions may await.
async fn main() is the one entry whose future the launcher drives automatically. There is no synchronous helper that blocks on a future.
async fn answer() -> int {
await sleep(5)
42
}
async fn main() {
println(await answer())
}
Open editable lesson →
Advanced concurrency · Async work, channels, and isolates
Chain a future
future.then(callback) transforms success; catch(callback) recovers an asynchronous failure. A callback may return a value or another future, which is flattened one level.
Expected domain failure still belongs in Result. Continuations keep async diagnostics until a final await decides whether the failure was recovered.
async fn number() -> int { 6 }
async fn main() {
let doubled = number().then(|value| value * 2)
println(await doubled)
}
Open editable lesson →
Advanced concurrency · Async work, channels, and isolates
Spawn and join tasks
spawn(future) starts cooperative work. join((a, b, ...)) starts every tuple child and returns results in source order, even when completion order differs. join_all does the same for a homogeneous array.
Failure or parent cancellation requests cancellation of unfinished siblings and drains their cleanup before the group settles.
async fn later(text: string, delay: int) -> string {
await sleep(delay)
text
}
async fn main() {
let results = await join((later("slow", 20), later("fast", 5)))
println("${results.0},${results.1}")
}
Open editable lesson →
Advanced concurrency · Async work, channels, and isolates
Select readiness and cancel
select returns the index of the first future to finish. cancel requests cooperative cancellation, and is_cancelled lets long-running work decide when to stop.
Cancellation is a request, so a task can finish its cleanup before it settles.
async fn later(value: int, delay: int) -> int {
await sleep(delay)
value
}
async fn main() {
let slow = spawn(later(10, 20))
let fast = spawn(later(20, 5))
println(await select([slow, fast]))
cancel(slow)
}
Open editable lesson →
Advanced concurrency · Async work, channels, and isolates
Bounded task channels
Channel<T> passes typed values between tasks. send waits while a bounded buffer is full, and recv waits while it is empty. Capacity zero hands each value directly from a sender to a receiver.
Closing preserves buffered values, then recv returns none. Sending after close is a runtime error.
async fn produce(channel: Channel<int>) {
await channel.send(1)
await channel.send(2)
channel.close()
}
async fn main() {
let channel = std.channel.new<int>(2)
spawn(produce(channel))
let mut total = 0
while let some(value) = await channel.recv() { total += value }
println(total)
}
Open editable lesson →
Advanced concurrency · Async work, channels, and isolates
Use cores with isolates
An isolate runs a named top-level function in parallel on an OS thread. Its mutable state is separate from the caller's. Plain data crosses by copy through arguments, results, or isolate channels.
Functions, futures, channels, and pointers cannot cross. Worker output is replayed as one block at join, so output is deterministic in join order. Thread creation is local-only here.
fn square(value: int) -> int { value * value }
fn main() {
let worker = std.isolate.spawn("square", [6])
println(std.isolate.join(worker))
}
Open editable lesson →
Advanced concurrency · Async work, channels, and isolates
Parallel collection work
Use async tasks to overlap waiting and std.parallel when collection work should use multiple CPU cores. It runs named functions through isolates for map, each, chunked, or long-lived workers.
Prefer chunked for many small items so each worker processes a slice instead of starting once per item. Results preserve item order.
fn squares(values: [int]) -> [int] {
[value * value for value in values]
}
fn main() {
println(std.parallel.chunked("squares", [1, 2, 3, 4], 2))
}
Open editable lesson →
Advanced concurrency · Async work, channels, and isolates
Share immutable data
std.shared.freeze(text) returns an ordinary immutable string that isolates can read without making a separate copy. put and get publish immutable strings under process-wide keys.
Published entries live for the process lifetime. Use it for large read-only data. It does not give workers shared mutable memory.
fn main() {
let text = std.shared.freeze("moon")
println(std.shared.is_shared(text))
std.shared.put("language", text)
println(std.shared.get("language").unwrap())
}
Open editable lesson →
Advanced native interop · Checked views and native boundaries
Checked views
&value lends read-only access, while &mut value lends exclusive access to a mutable value. The corresponding parameter types are &T and &mut T. The compiler checks that a view is used only while its storage is valid.
A view does not keep a local alive after its scope ends. These checks also apply when a view is passed through a helper or stored in another value. Raw foreign pointers are a separate type family and prove no ownership or lifetime.
This example checks a mutation through a view without calling a native library.
struct Counter { value: int }
fn add(counter: &mut Counter, amount: int) {
counter.value += amount
}
fn main() {
let mut counter = Counter { value: 1 }
add(&mut counter, 2)
}
Open editable lesson →
Advanced native interop · Checked views and native boundaries
Mutable receiver methods
Use mut self for a method that changes a struct value. A call such as counter.bump() borrows the receiver automatically and evaluates it once.
The receiver must permit mutation. You do not need a raw pointer or an ownership annotation to call the method.
struct Counter { value: int }
impl Counter {
fn bump(mut self) { self.value += 1 }
}
fn main() {
let mut counter = Counter { value: 1 }
counter.bump()
}
Open editable lesson →
Advanced native interop · Checked views and native boundaries
External declarations
extern fn describes a function supplied outside Moon. Use types that match the actual native declaration. The example checks a signature with a signed 32-bit argument and result; it does not load or call a library.
A declaration alone supplies neither an implementation nor a lifetime guarantee. Raw foreign calls require unsafe, and executable calls also require a native binding. The current moon run does not discover native libraries from source declarations. An incomplete-lifetime warning from moon check is not proof that an external call is safe.
extern fn native_double(value: i32) -> i32
fn main() {}
Open editable lesson →
Advanced native interop · Checked views and native boundaries
Headers and libraries
Header paths, search roots, library linkage and target selection belong to native build inputs. Source annotations such as @include, @link and their static or platform variants have been retired and are rejected.
The current source runner does not yet provide native-module configuration. LLVM tool locations belong in [toolchain.llvm] in moon.toml; that setting does not itself import a header or load a library.
The declaration below can be checked as source. Calling it needs an implementation and a native binding; adding a source directive cannot provide either.
extern fn native_sqrt(value: float) -> float
fn main() {}
Open editable lesson →
Advanced native interop · Checked views and native boundaries
Moon values and C layouts
An ordinary Moon struct has value semantics. Giving its fields C-like widths does not assert a C ABI or packed representation. @C and @packed are retired.
Foreign records take their size, alignment, field offsets and packing from the imported C declaration. Their imported names are used directly as values. Source-level header loading is still unavailable in the current runner, so this example demonstrates an ordinary Moon value only.
Do not substitute this struct for a C record merely because its fields look alike.
struct Point { x: i32, y: i32 }
fn main() {
let point = Point { x: 3, y: 4 }
let copy = point
}
Open editable lesson →
Advanced native interop · Checked views and native boundaries
C variadic declarations
A bare trailing ... on an extern declaration describes C varargs. It does not collect a Moon array. The declaration below can be checked, but this lesson does not execute a variadic native call.
Moon int is signed 32-bit, long is signed 64-bit, and float is binary64. A C type's width depends on the selected target; matching a name is not enough. Native argument promotions and format strings must agree with the actual imported ABI. Do not assume a Moon string is a C char*.
extern fn log_values(count: i32, ...) -> i32
fn main() {}
Open editable lesson →
Advanced native interop · Checked views and native boundaries
Callbacks and retention
A function parameter can describe a callback's argument and result types. That signature says nothing about whether foreign code retains the callback or invokes it from another thread. The example checks a declaration only.
@queued, @blocking and @ffi_retain_callbacks are retired. A retained callback needs an owning registration that keeps its captures alive until the native side acknowledges that calls have stopped. A queued callback also needs checked payload transfer and queue ownership.
These source APIs are not available yet. Removing the old annotation does not make retaining a callback safe, and unsafe does not establish a lifetime proof. Do not pass a closure to a retaining native API using this declaration alone.
extern fn store_callback(callback: fn(i32) -> i32) -> i32
fn main() {}
Open editable lesson →
Put it together · A small program and local workflow
A small multi-file program
This final program has no new syntax. One module owns the domain types and lookup; the entry module waits for data, calls the fallible operation, and matches its result.
Edit the requested title or the collection. Both files travel in the same bounded request, exactly as they would in a local project.
use catalog.{Book, Query, find}
async fn load() -> [Book] {
await sleep(1)
[
Book { title: "moon", tags: ["language", "tour"] },
Book { title: "Stars", tags: ["space"] },
]
}
async fn main() {
let books = await load()
match find(books, .Title("moon")) {
ok(book) => println("${book.title}: ${book.tags.join(",")}"),
err(error) => println("${error.name}: ${error.message}"),
}
}
Open editable lesson →
Put it together · A small program and local workflow
Write project tests
moon test directory discovers fn test_* suites; they need no main. Use std.testing assertions and --filter text to select one suite or test while iterating.
Keep each test about one property, even when that property needs several assertions. A failing assertion names the test and stops that test with a useful diagnostic.
use std.testing.{assert_eq, assert_true}
fn double(value: int) -> int { value * 2 }
fn test_double() {
assert_eq(double(4), 8)
assert_true(double(-2) < 0)
}
Open editable lesson →
Put it together · A small program and local workflow
Continue locally
moon new creates a small project. check catches source errors, run compiles and launches, and build leaves standalone executables. fmt --check and lint keep style and policy visible.
Use moon doc for API details, and the language reference or editor navigation when you need an exact rule or declaration.
fn main() {
println("Hello moon")
}
Open editable lesson →
Put it together · A small program and local workflow
Editor support and references
moon ships integrations for VS Code, Neovim, Zed, and JetBrains. They share the compiler's language server for diagnostics, completion, navigation, rename, formatting, semantic highlighting, and inlay information.
The tour covers the language surface once, so use the language reference for exact rules and generated moon doc output for complete standard-library signatures.
fn main() {
let message: string = "Your editor asks moon lsp for language facts."
println(message)
}
Open editable lesson →